InfoTestProcess

Introduction: Retrieve information of the Test process in your Build Scan
More: Author   ReportBugs   
Tags:

Captures per-worker JVM metrics for every Gradle test fork in your build and publishes them as Build Scan® custom values, ready for inspection in the scan UI or aggregation in Develocity Analytics (DRV).

For each test worker JVM the plugin records:

  • Identity — task path, executor name, configured -Xmx, JVM args, start time, PID
  • CPU — total CPU time (cpuTimeSec) and average cores busy (cpuCoresAvg)
  • Memory — heap usage at shutdown, peak heap across pools, peak metaspace
  • GC — collector type (G1 / Parallel / Z / Shenandoah / CMS / Serial), collection count, total GC time
  • JIT — total compilation time
  • Class loading — total classes loaded
  • Threads — peak thread count

[!NOTE] Since version 1.0.0 the plugin is applied in settings.gradle(.kts).

Usage

Apply the plugin in your main settings.gradle(.kts):

plugins {
    id("io.github.cdsap.testprocess") version "2.1.0"
}

That's it. The plugin attaches to every Test task in the build and produces legacy output in two modes:

Mode Output
Develocity applied Custom values + scan tags published to the Build Scan
No Develocity Structured JSON file at ${rootDir}/build/info-test-process/statsTestTasks.json

GBOS output is experimental and disabled by default (gbos.enabled=false). Enabling it is opt-in and does not change legacy JSON, Build Scan custom values, or tags. File sinks and Develocity GBOS publishing can be enabled independently during same-major adoption. Prefer Gradle properties for CI-friendly opt-in.

Output

Build Scan custom values

Summary scalars (indexable in DRV)

Key Description
testProcess.workers.count Number of test worker JVMs registered
testProcess.workers.tasksWith Number of distinct test tasks that ran
testProcess.workers.snapshotsMissing Workers that didn't write a stats snapshot (health signal)
testProcess.worker.shown Workers whose detail blob was emitted (== count unless truncated)
testProcess.cpuCoresAvg.max Highest cpuTime / uptime across all workers (>1 = multi-core utilization)
testProcess.cpuTimeSec.sum Total CPU time across all workers
testProcess.heapPeakGb.max Highest per-worker peak heap usage
testProcess.metaspacePeakMb.max Highest per-worker peak metaspace
testProcess.jitSec.sum Total JIT compilation time across workers
testProcess.jitSec.max Worst single-worker JIT time
testProcess.classesLoaded.max Largest classloader footprint
testProcess.gcCollections.sum Total GC collections across workers
testProcess.uptimeMin.sum Total worker wall-clock time

Per-worker detail

One custom value per worker, key = testProcess.worker.<pid>, value = JSON:

{
  "pid": 13402,
  "task": ":core:domain:testDemoDebugUnitTest",
  "executor": "Gradle Test Executor 5",
  "xmx": "512m",
  "uptimeMin": 0.02,
  "cpuTimeSec": 3.05,
  "cpuCoresAvg": 3.31,
  "heapUsageGb": 0.14,
  "heapPeakGb": 0.14,
  "metaspacePeakMb": 14.54,
  "gcType": "G1",
  "gcCollections": 2,
  "gcTimeSec": 0.0,
  "jitSec": 2.28,
  "classesLoaded": 3503,
  "peakThreads": 10,
  "statsSnapshotMissing": false
}

If a build exceeds Develocity's 1,000-unique-values-per-scan cap (i.e. >950 workers), the heaviest workers by cpuTimeSec are kept and the plugin emits testProcess.worker.truncated=true plus testProcess.worker.total=<count> so you know data was dropped.

Optional GBOS projection (experimental)

GBOS (Gradle Build Observability Schema) is an experimental, opt-in spike. It is disabled by default — installs with no GBOS properties keep the same legacy output as before.

Set infoTestProcess.gbos.develocity.enabled=true in gradle.properties (or -PinfoTestProcess.gbos.develocity.enabled=true) to also publish the GBOS Develocity projection. This flag is independent of file output and of infoTestProcess.gbos.enabled. Legacy testProcess.* custom values and scan tags remain unchanged and continue to be the supported default.

The projection emits canonical observations as repeated gbos.v1.observation custom values. Any PID, task path, or test executor identity is stored inside the observation JSON attributes, not in custom-value names. It also emits only the allowlisted build-level scalar indexes:

Key Source measurement
gbos.v1.index.info_test_process.jvm.process.cpu.cores.max jvm.process.cpu.cores / max
gbos.v1.index.info_test_process.jvm.process.cpu.time.sum jvm.process.cpu.time / sum
gbos.v1.index.info_test_process.jvm.process.memory.heap.peak.max jvm.process.memory.heap.peak / max

Optional GBOS file output

GBOS file output is off unless you opt in. The simplest CI-friendly switch:

infoTestProcess.gbos.enabled=true

That enables both JSON and NDJSON sinks. You can also target a single sink (these override the master flag when set):

infoTestProcess.gbos.json.enabled=true
infoTestProcess.gbos.ndjson.enabled=true

The same options are available on the Settings extension:

infoTestProcess {
    gbos {
        enabled.set(true)
        // develocity.set(true) // independent of file sinks
    }
}

When disabled, no GBOS files are written and legacy output is unchanged. The JSON sink writes a GBOS report envelope to ${rootDir}/build/info-test-process/gbos.json:

{
  "schemaVersion": "1.0.0",
  "resource": {
    "build.tool.name": "gradle"
  },
  "observations": [
    {
      "schemaVersion": "1.0.0",
      "producer": {
        "name": "info-test-process",
        "version": "2.1.0"
      },
      "scope": "jvm.process",
      "aggregationScope": "entity",
      "attributes": {
        "process.pid": 13402,
        "jvm.process.role": "test-worker",
        "gradle.task.path": ":core:test",
        "gradle.test.executor": "Gradle Test Executor 5"
      },
      "measurements": [
        {
          "name": "jvm.process.cpu.time",
          "value": 3.05,
          "unit": "s",
          "aggregation": "sum"
        }
      ]
    }
  ]
}

The NDJSON sink writes compact observations, one per line, to ${rootDir}/build/info-test-process/gbos.ndjson. Both file outputs are generated from the same internal GBOS observation model used by the Develocity projection.

Generated GBOS examples are validated in CI against the public schema contract (test-only; not a runtime dependency). See docs/gbos.md for opt-in flags, compatibility rules, and legacy key retention.

Scan tags

Categorical filters for fast slicing in DRV. Fired automatically when thresholds hit:

Tag When
tests:cpu-heavy Any worker cpuCoresAvg > 4.0
tests:near-oom Any worker heapPeak / xmx ≥ 0.8
tests:jit-bound Any worker jitSec / cpuTimeSec ≥ 0.5 and jitSec ≥ 1.0
tests:no-snapshot One or more workers didn't write a stats snapshot

File output (no Develocity)

Without Develocity, the same data is written to ${rootDir}/build/info-test-process/statsTestTasks.json as a single nested document with summary / byTask / workers / tags (the file lives under build/ so it's covered by every project's existing .gitignore and wiped by ./gradlew clean):

{
  "summary": {
    "workers": {
      "count": 1,
      "tasksWith": 1,
      "snapshotsMissing": 0
    },
    "cpuCoresAvgMax": 2.41,
    "cpuTimeSecSum": 0.42,
    "heapPeakGbMax": 0.02,
    "metaspacePeakMbMax": 7.2,
    "jitSecSum": 0.2,
    "jitSecMax": 0.2,
    "classesLoadedMax": 2711,
    "gcCollectionsSum": 0,
    "uptimeMinSum": 0.0
  },
  "byTask": {
    ":test": {
      "workers": 1,
      "cpuCoresAvgMax": 2.41,
      "cpuTimeSecSum": 0.42,
      "heapPeakGbMax": 0.02,
      "jitSecMax": 0.2,
      "classesLoadedMax": 2711,
      "peakThreadsMax": 9
    }
  },
  "workers": [
    {
      "pid": 20910,
      "task": ":test",
      "executor": "Gradle Test Executor 24",
      "xmx": "512m",
      "uptimeMin": 0.0,
      "cpuTimeSec": 0.42,
      "cpuCoresAvg": 2.41,
      "heapUsageGb": 0.02,
      "heapPeakGb": 0.02,
      "metaspacePeakMb": 7.2,
      "gcType": "G1",
      "gcCollections": 0,
      "gcTimeSec": 0.0,
      "jitSec": 0.2,
      "classesLoaded": 2711,
      "peakThreads": 9,
      "statsSnapshotMissing": false
    }
  ],
  "tags": []
}

byTask is intentionally not emitted on the scan — it's derivable from the per-PID values via DRV (see below) and would duplicate data against the per-scan value cap. It is included in the file output because the file isn't subject to scan limits and the precomputed summary aids local inspection.

Querying from Develocity Analytics (DRV)

Find builds with heavy workers

SELECT build_id
FROM gradle_attributes_custom_values
WHERE name = 'testProcess.cpuCoresAvg.max'
  AND CAST(value AS DECIMAL) > 4;

Reconstruct per-task aggregates on the fly

SELECT
  JSON_VALUE(value, '$.task')                  AS task,
  COUNT(*)                                     AS workers,
  MAX(JSON_VALUE(value, '$.cpuCoresAvg'))      AS cpu_cores_max,
  SUM(JSON_VALUE(value, '$.cpuTimeSec'))       AS cpu_time_sum,
  MAX(JSON_VALUE(value, '$.heapPeakGb'))       AS heap_peak_max,
  MAX(JSON_VALUE(value, '$.jitSec'))           AS jit_max
FROM gradle_attributes_custom_values
WHERE build_id = '<scan-id>'
  AND name LIKE 'testProcess.worker.%'
  AND name NOT IN (
    'testProcess.worker.shown',
    'testProcess.worker.truncated',
    'testProcess.worker.total'
  )
GROUP BY task;

Filter to "cpu-heavy or jit-bound" builds

SELECT build_id FROM builds WHERE tags && ARRAY['tests:cpu-heavy', 'tests:jit-bound'];

How it works

The plugin uses a small Java agent (~5 KB, no runtime dependencies) that is injected into every Gradle test worker JVM via -javaagent:. The agent:

  1. On JVM startup (premain), reads a sentinel system property to learn its task path and writes a JSON identity file to a known directory.
  2. On JVM shutdown, reads RuntimeMXBean / MemoryMXBean / GarbageCollectorMXBean / OperatingSystemMXBean / CompilationMXBean / ClassLoadingMXBean / ThreadMXBean and writes a stats snapshot to the same directory.

At the end of the build, the plugin reads those files, joins identity with stats by PID, and publishes the data. This avoids:

  • The race conditions of polling ProcessHandle.descendants() from the daemon
  • External jstat shell-outs (and the bash/Windows portability headaches)
  • Misattribution from parsing JVM tmpdir paths to guess task identity

The agent itself runs in the worker JVM and has no Kotlin / kotlinx dependency — it adds nothing to the worker's bootclasspath beyond a single ~5 KB class file.

Configuration cache compatibility

Mode Status
--configuration-cache (basic) ✅ Stored on first build, reused on subsequent builds
--configuration-cache-problems=fail (strict) ✅ No problems reported
-Dorg.gradle.unsafe.isolated-projects=true (Isolated Projects)
Gradle 8.x, 9.x ✅ Both verified in the integration test suite

The plugin does no filesystem I/O at apply time — agent jar extraction and registry directory setup are deferred to a BuildService init block, which runs at task execution time (after CC restoration).

Requirements

  • Gradle 8 or later
  • Test workers running JDK 17 or later (the bundled Java agent is compiled to Java 17 bytecode)
  • Optional: Develocity, if you want the data on Build Scans

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools
AI Daily Digest