Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
388eb39
Phase 1: instrumentation, coverage collection and logging
nikita-tkachenko-datadog Mar 23, 2026
bde7b53
Implement sending coverage report
nikita-tkachenko-datadog Mar 23, 2026
632c3a9
Implement probe-to-line cache
nikita-tkachenko-datadog Mar 23, 2026
d0bf930
Replace LCOV with compact binary coverage protocol
nikita-tkachenko-datadog Mar 24, 2026
d9e68ea
Add tests for the binary cov data encoder
nikita-tkachenko-datadog Mar 24, 2026
ba20380
Fix NPE in probe cache for classes with no executable lines
nikita-tkachenko-datadog Mar 24, 2026
e465278
Skip classes with null sourceFile, log instrumentation failures
nikita-tkachenko-datadog Mar 24, 2026
f3c6408
Replace N+1 Analyzer passes with single-pass CFG walking for probe cache
nikita-tkachenko-datadog Mar 25, 2026
0e2743d
Resolve cache misses via classloader before falling back to classpath…
nikita-tkachenko-datadog Mar 25, 2026
566c543
Set language/env/flag tags
nikita-tkachenko-datadog Mar 25, 2026
9e26d80
Use AgentTaskScheduler for coverage collection thread
nikita-tkachenko-datadog Mar 25, 2026
b5017ef
Minor tweak
nikita-tkachenko-datadog Mar 25, 2026
d7a063a
Moved a couple of methods around
nikita-tkachenko-datadog Mar 26, 2026
6034289
Replace ClassProbeMappingBuilder with JaCoCo Analyzer delegation
nikita-tkachenko-datadog Mar 26, 2026
1a38ffe
Fix single-pass ClassProbeMappingBuilder probe semantics
nikita-tkachenko-datadog Mar 26, 2026
39fb2bb
Report executable lines for instrumented classes with no probe hits
nikita-tkachenko-datadog Mar 26, 2026
bb39c2b
Resolve class bytes via defining ClassLoader recorded at transform time
nikita-tkachenko-datadog Mar 26, 2026
88fb7af
Remove dead dd.code.coverage.classpath configuration
nikita-tkachenko-datadog Mar 26, 2026
3b91f88
Avoid round-trip when collecting code coverage
nikita-tkachenko-datadog Mar 26, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import datadog.instrument.utils.ClassLoaderValue;
import datadog.metrics.api.statsd.StatsDClientManager;
import datadog.trace.api.Config;
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.api.Platform;
import datadog.trace.api.WithGlobalTracer;
import datadog.trace.api.appsec.AppSecEventTracker;
Expand Down Expand Up @@ -336,6 +337,11 @@ public static void start(
StaticEventLogger.end("crashtracking");
}

Object codeCoverageTransformer = null;
if (InstrumenterConfig.get().isCodeCoverageEnabled()) {
codeCoverageTransformer = maybeStartCodeCoverage(inst);
}

startDatadogAgent(initTelemetry, inst);

final EnumSet<Library> libraries = detectLibraries(log);
Expand Down Expand Up @@ -390,7 +396,8 @@ public static void start(
}

InstallDatadogTracerCallback installDatadogTracerCallback =
new InstallDatadogTracerCallback(initTelemetry, inst, okHttpDelayMillis);
new InstallDatadogTracerCallback(
initTelemetry, inst, okHttpDelayMillis, codeCoverageTransformer);
if (waitForJUL) {
log.debug("Custom logger detected. Delaying Datadog Tracer initialization.");
registerLogManagerCallback(installDatadogTracerCallback);
Expand Down Expand Up @@ -645,11 +652,14 @@ protected static class InstallDatadogTracerCallback extends ClassLoadCallBack {
private final Object sco;
private final Class<?> scoClass;
private final int okHttpDelayMillis;
private final Object codeCoverageTransformer;

public InstallDatadogTracerCallback(
InitializationTelemetry initTelemetry,
Instrumentation instrumentation,
int okHttpDelayMillis) {
int okHttpDelayMillis,
Object codeCoverageTransformer) {
this.codeCoverageTransformer = codeCoverageTransformer;
this.okHttpDelayMillis = okHttpDelayMillis;
this.instrumentation = instrumentation;
try {
Expand Down Expand Up @@ -696,6 +706,10 @@ public void execute() {
if (flareEnabled) {
startFlarePoller(scoClass, sco);
}

if (codeCoverageTransformer != null) {
startCodeCoverageCollector(codeCoverageTransformer, sco);
}
}

private void resumeRemoteComponents() {
Expand Down Expand Up @@ -1124,6 +1138,34 @@ private static void maybeStartCiVisibility(Instrumentation inst, Class<?> scoCla
}
}

private static Object maybeStartCodeCoverage(Instrumentation inst) {
StaticEventLogger.begin("Code Coverage");

try {
final Class<?> systemClass =
AGENT_CLASSLOADER.loadClass("datadog.trace.codecoverage.CodeCoverageSystem");
final Method startMethod = systemClass.getMethod("start", Instrumentation.class);
return startMethod.invoke(null, inst);
} catch (final Throwable e) {
log.warn("Not starting Code Coverage subsystem", e);
return null;
} finally {
StaticEventLogger.end("Code Coverage");
}
}

private static void startCodeCoverageCollector(Object transformer, Object sco) {
try {
final Class<?> systemClass =
AGENT_CLASSLOADER.loadClass("datadog.trace.codecoverage.CodeCoverageSystem");
final Method startCollectorMethod =
systemClass.getMethod("startCollector", Object.class, Object.class);
startCollectorMethod.invoke(null, transformer, sco);
} catch (final Throwable e) {
log.warn("Not starting Code Coverage collector", e);
}
}

private static void maybeStartLLMObs(Instrumentation inst, Class<?> scoClass, Object sco) {
if (llmObsEnabled) {
StaticEventLogger.begin("LLM Observability");
Expand Down
1 change: 1 addition & 0 deletions dd-java-agent/agent-ci-visibility/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation project(':components:json')
implementation project(':internal-api')
implementation project(':internal-api:internal-api-9')
implementation project(':utils:coverage-utils')

testImplementation project(':dd-java-agent:testing')
testImplementation("com.google.jimfs:jimfs:1.1") // an in-memory file system for testing code that works with files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
import datadog.trace.civisibility.coverage.SkippableAwareCoverageStoreFactory;
import datadog.trace.civisibility.coverage.file.FileCoverageStore;
import datadog.trace.civisibility.coverage.line.LineCoverageStore;
import datadog.communication.http.OkHttpUtils;
import datadog.trace.api.civisibility.telemetry.CiVisibilityCountMetric;
import datadog.trace.api.civisibility.telemetry.CiVisibilityDistributionMetric;
import datadog.trace.civisibility.communication.TelemetryListener;
import datadog.trace.civisibility.coverage.report.CoverageProcessor;
import datadog.trace.civisibility.coverage.report.CoverageReportUploader;
import datadog.trace.civisibility.coverage.report.JacocoCoverageProcessor;
import datadog.trace.coverage.CoverageReportUploader;
import datadog.trace.civisibility.coverage.report.child.ChildProcessCoverageReporter;
import datadog.trace.civisibility.coverage.report.child.JacocoChildProcessCoverageReporter;
import datadog.trace.civisibility.domain.buildsystem.ModuleSignalRouter;
Expand All @@ -34,11 +38,20 @@ static class Parent {

ExecutionSettings executionSettings =
repoServices.executionSettingsFactory.create(JvmInfo.CURRENT_JVM, null);
CoverageReportUploader coverageReportUploader =
executionSettings.isCodeCoverageReportUploadEnabled()
? new CoverageReportUploader(
services.ciIntake, repoServices.ciTags, services.metricCollector)
: null;
CoverageReportUploader coverageReportUploader;
if (executionSettings.isCodeCoverageReportUploadEnabled()) {
OkHttpUtils.CustomListener telemetryListener =
new TelemetryListener.Builder(services.metricCollector)
.requestCount(CiVisibilityCountMetric.COVERAGE_UPLOAD_REQUEST)
.requestBytes(CiVisibilityDistributionMetric.COVERAGE_UPLOAD_REQUEST_BYTES)
.requestErrors(CiVisibilityCountMetric.COVERAGE_UPLOAD_REQUEST_ERRORS)
.requestDuration(CiVisibilityDistributionMetric.COVERAGE_UPLOAD_REQUEST_MS)
.build();
coverageReportUploader =
new CoverageReportUploader(services.ciIntake, repoServices.ciTags, telemetryListener);
} else {
coverageReportUploader = null;
}

coverageProcessorFactory =
new JacocoCoverageProcessor.Factory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import datadog.trace.api.civisibility.domain.SourceSet;
import datadog.trace.civisibility.config.ExecutionSettings;
import datadog.trace.civisibility.domain.buildsystem.ModuleSignalRouter;
import datadog.trace.coverage.CoverageReportUploader;
import datadog.trace.coverage.LcovReportWriter;
import datadog.trace.coverage.LinesCoverage;
import datadog.trace.civisibility.ipc.AckResponse;
import datadog.trace.civisibility.ipc.ModuleCoverageDataJacoco;
import datadog.trace.civisibility.ipc.SignalResponse;
Expand Down
31 changes: 31 additions & 0 deletions dd-java-agent/agent-code-coverage/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
id 'com.gradleup.shadow'
}

apply from: "$rootDir/gradle/java.gradle"
apply from: "$rootDir/gradle/version.gradle"

minimumBranchCoverage = 0.0
minimumInstructionCoverage = 0.0

dependencies {
api libs.slf4j

implementation group: 'org.jacoco', name: 'org.jacoco.core', version: '0.8.14'

implementation project(':internal-api')
implementation project(':communication')
implementation project(':utils:coverage-utils')

testImplementation project(':dd-java-agent:testing')
}

tasks.named("shadowJar", ShadowJar) {
dependencies deps.excludeShared
}

tasks.named("jar", Jar) {
archiveClassifier = 'unbundled'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package datadog.trace.codecoverage;

import java.util.BitSet;

/**
* Cached mapping from probe IDs to source lines for a single class. Built once per class version
* (identified by CRC64) and reused across collection cycles.
*/
final class ClassProbeMapping {
final long classId;
final String className; // "com/example/MyClass"
final String sourceFile; // "SourceFile.java"
final BitSet executableLines;
final int[][] probeToLines; // probeToLines[probeId] = sorted line numbers

ClassProbeMapping(
long classId,
String className,
String sourceFile,
BitSet executableLines,
int[][] probeToLines) {
this.classId = classId;
this.className = className;
this.sourceFile = sourceFile;
this.executableLines = executableLines;
this.probeToLines = probeToLines;
}
}
Loading
Loading