Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## Unreleased

### Enhancements

* Add continuous profiling (`enableContinuousProfiling`, `continuousProfilingMaxAgeSeconds`) which
keeps a single JFR recording running in a circular buffer so profile requests dump the most recent
window of data immediately
([#4807](https://github.com/microsoft/ApplicationInsights-Java/pull/4807))

### Breaking changes

* Rename the `MachineStats` JFR diagnostic event to `MachineInfo`. Custom `.jfc` files that enable
`com.microsoft.applicationinsights.diagnostics.jfr.MachineStats` must be updated to the new event
name ([#4807](https://github.com/microsoft/ApplicationInsights-Java/pull/4807))

## Version 3.7.9 GA (06/18/2026)

### Enhancements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,17 @@ public interface DiagnosticEngine {
* alertBreach.alertConfiguration.profileDuration
*/
Future<DiagnosisResult<?>> performDiagnosis(AlertBreach alertBreach);

/**
* Start collecting diagnostics continuously.
*
* <p>Used with continuous profiling, where a circular buffer is dumped on demand rather than a
* forward-looking recording being created per breach. Registering the periodic diagnostic
* emitters up front ensures diagnostic events populate the continuous recording buffer, so they
* are present in any snapshot that is dumped. Defaults to a no-op.
*/
default void startContinuousDiagnostics() {}

/** Stop collecting diagnostics continuously. Defaults to a no-op. */
default void stopContinuousDiagnostics() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,73 @@
import jdk.jfr.StackTrace;

@SuppressWarnings("Java8ApiChecker") // JFR APIs require Java 11+, but agent targets Java 8 bytecode
@Name("com.microsoft.applicationinsights.diagnostics.jfr.MachineStats")
@Label("MachineStats")
@Name("com.microsoft.applicationinsights.diagnostics.jfr.MachineInfo")
@Label("MachineInfo")
@Category("Diagnostic")
@Description("MachineStats")
@Description("MachineInfo")
@StackTrace(false)
@Period("beginChunk")
public class MachineStats extends Event implements JsonSerializable<MachineStats> {
public static final String NAME =
public class MachineInfo extends Event implements JsonSerializable<MachineInfo> {
public static final String NAME = "com.microsoft.applicationinsights.diagnostics.jfr.MachineInfo";

/**
* Event name emitted by agents prior to the MachineStats-&gt;MachineInfo rename. Readers fall
* back to this so previously-recorded recordings (which carry a "MachineStats" event) can still
* be located and scored.
*/
public static final String LEGACY_NAME =
"com.microsoft.applicationinsights.diagnostics.jfr.MachineStats";
private double contextSwitchesPerMs;

/**
* Current schema version. Version 2 drops the legacy {@code contextSwitchesPerMs} field;
* recordings produced before the schemaVersion field was added carry schemaVersion 1 (the
* implicit legacy version) and still serialize {@code contextSwitchesPerMs}.
*/
public static final int SCHEMA_VERSION = 2;

private int coreCount;

public double getContextSwitchesPerMs() {
return contextSwitchesPerMs;
private int schemaVersion;

public int getCoreCount() {
return coreCount;
}

public MachineStats setContextSwitchesPerMs(double contextSwitchesPerMs) {
this.contextSwitchesPerMs = contextSwitchesPerMs;
public MachineInfo setCoreCount(int coreCount) {
this.coreCount = coreCount;
return this;
}

public int getCoreCount() {
return coreCount;
public int getSchemaVersion() {
return schemaVersion;
}

public MachineStats setCoreCount(int coreCount) {
this.coreCount = coreCount;
public MachineInfo setSchemaVersion(int schemaVersion) {
this.schemaVersion = schemaVersion;
return this;
}

@Override
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
jsonWriter.writeStartObject();
return jsonWriter
.writeStartObject()
.writeDoubleField("contextSwitchesPerMs", contextSwitchesPerMs)
.writeIntField("coreCount", coreCount)
.writeIntField("schemaVersion", schemaVersion)
.writeEndObject();
}

public static MachineStats fromJson(JsonReader jsonReader) throws IOException {
public static MachineInfo fromJson(JsonReader jsonReader) throws IOException {
return jsonReader.readObject(
reader -> {
MachineStats deserializedValue = new MachineStats();
MachineInfo deserializedValue = new MachineInfo();

while (reader.nextToken() != JsonToken.END_OBJECT) {
String fieldName = reader.getFieldName();
reader.nextToken();
if ("contextSwitchesPerMs".equals(fieldName)) {
deserializedValue.setContextSwitchesPerMs(reader.getDouble());
} else if ("coreCount".equals(fieldName)) {
if ("coreCount".equals(fieldName)) {
deserializedValue.setCoreCount(reader.getInt());
} else if ("schemaVersion".equals(fieldName)) {
deserializedValue.setSchemaVersion(reader.getInt());
} else {
reader.skipChildren();
}
Expand Down
5 changes: 5 additions & 0 deletions agent/agent-profiler/agent-diagnostics/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ dependencies {

compileOnly("com.google.auto.service:auto-service")
annotationProcessor("com.google.auto.service:auto-service")

testImplementation("org.assertj:assertj-core")
testImplementation("org.mockito:mockito-core")
testImplementation("org.gradle.jfr.polyfill:jfr-polyfill:1.0.2")
testImplementation("com.azure:azure-json")
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.microsoft.applicationinsights.diagnostics.DiagnosticEngine;
import com.microsoft.applicationinsights.diagnostics.jfr.AlertBreachJfrEvent;
import com.microsoft.applicationinsights.diagnostics.jfr.CodeOptimizerDiagnosticsJfrInit;
import com.microsoft.applicationinsights.diagnostics.jfr.MachineStats;
import com.microsoft.applicationinsights.diagnostics.jfr.MachineInfo;
import com.microsoft.applicationinsights.diagnostics.jfr.SystemStatsProvider;
import java.io.IOException;
import java.io.StringWriter;
Expand All @@ -20,6 +20,8 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -35,7 +37,16 @@ public class CodeOptimizerDiagnosticEngineJfr implements DiagnosticEngine {
private final ScheduledExecutorService executorService;
private final Semaphore semaphore = new Semaphore(1, false);
private final Path cgroupBasePath;
private int thisPid;
private final AtomicInteger thisPid = new AtomicInteger();

// When true, periodic diagnostic emitters are registered continuously (for continuous profiling)
// and must not be torn down by an individual performDiagnosis cycle.
private final AtomicBoolean continuous = new AtomicBoolean(false);

// Guards transitions of the continuous flag against the teardown performed at the end of a
// (non-continuous) diagnostic cycle, so that a breach processed during startup cannot tear down
// the continuously-registered emitters once continuous profiling has been enabled.
private final Object continuousLifecycleLock = new Object();

public CodeOptimizerDiagnosticEngineJfr(
ScheduledExecutorService executorService, Path cgroupBasePath) {
Expand All @@ -45,42 +56,97 @@ public CodeOptimizerDiagnosticEngineJfr(

@Override
public void init(int thisPid) {
if (!CodeOptimizerDiagnosticsJfrInit.isOsSupported()) {
if (!isOsSupported()) {
logger.warn("Code Optimizer diagnostics is not supported on this operating system");
return;
}

this.thisPid = thisPid;
this.thisPid.set(thisPid);

logger.debug("Initialising Code Optimizer Diagnostic Engine");
CodeOptimizerDiagnosticsJfrInit.initFeature(thisPid, cgroupBasePath);
logger.debug("Code Optimizer Diagnostic Engine Initialised");
}

private static void startDiagnosticCycle(int thisPid, Path cgroupBasePath) {
// visible for testing
protected boolean isOsSupported() {
return CodeOptimizerDiagnosticsJfrInit.isOsSupported();
}

// visible for testing
protected void startDiagnosticCycle() {
logger.debug("Starting Code Optimizer Diagnostic Cycle");
CodeOptimizerDiagnosticsJfrInit.initFeature(thisPid, cgroupBasePath);
CodeOptimizerDiagnosticsJfrInit.start(thisPid, cgroupBasePath);
int pid = thisPid.get();
CodeOptimizerDiagnosticsJfrInit.initFeature(pid, cgroupBasePath);
CodeOptimizerDiagnosticsJfrInit.start(pid, cgroupBasePath);
}

private static void endDiagnosticCycle() {
// visible for testing
protected void endDiagnosticCycle() {
logger.debug("Ending Code Optimizer Diagnostic Cycle");
CodeOptimizerDiagnosticsJfrInit.stop();
}

@Override
public void startContinuousDiagnostics() {
if (!isOsSupported()) {
logger.warn("Code Optimizer diagnostics is not supported on this operating system");
return;
}

synchronized (continuousLifecycleLock) {
continuous.set(true);
logger.debug("Starting continuous Code Optimizer diagnostics");
// Registers the periodic diagnostic emitters (Telemetry, CGroupData) so they continuously
// populate the continuous profiling circular buffer.
startDiagnosticCycle();
}
}

@Override
public void stopContinuousDiagnostics() {
if (!isOsSupported()) {
return;
}

synchronized (continuousLifecycleLock) {
continuous.set(false);
logger.debug("Stopping continuous Code Optimizer diagnostics");
endDiagnosticCycle();
}
}

@Override
public Future<DiagnosisResult<?>> performDiagnosis(AlertBreach alert) {
if (continuous.get()) {
// Periodic diagnostics are already running continuously, so we must not start or stop the
// diagnostic cycle here (doing so would remove the continuously-registered emitters). Just
// emit the point-in-time breach information.
CompletableFuture<DiagnosisResult<?>> diagnosisResultCompletableFuture =
new CompletableFuture<>();
try {
emitInfo(alert);
diagnosisResultCompletableFuture.complete(null);
} catch (RuntimeException e) {
// The caller discards the returned future, so log here to avoid silently swallowing the
// failure to emit breach diagnostics.
logger.error("Failed to emit continuous diagnostic breach information", e);
diagnosisResultCompletableFuture.completeExceptionally(e);
}
return diagnosisResultCompletableFuture;
}

CompletableFuture<DiagnosisResult<?>> diagnosisResultCompletableFuture =
new CompletableFuture<>();
try {
if (semaphore.tryAcquire(SEMAPHORE_TIMEOUT_IN_SEC, TimeUnit.SECONDS)) {
emitInfo(alert, cgroupBasePath);
emitInfo(alert);

long profileDurationInSec = alert.getAlertConfiguration().getProfileDurationSeconds();

long end = profileDurationInSec - TIME_BEFORE_END_OF_PROFILE_TO_EMIT_EVENT;

startDiagnosticCycle(thisPid, cgroupBasePath);
startDiagnosticCycle();

scheduleEmittingAlertBreachEvent(alert, end);

Expand All @@ -105,13 +171,25 @@ private void scheduleShutdown(
executorService.schedule(
() -> {
try {
emitInfo(alert, cgroupBasePath);
emitInfo(alert);

// We do not return a result atm
diagnosisResultCompletableFuture.complete(null);

logger.debug("Shutting down diagnostic cycle");
endDiagnosticCycle();
// Only tear down the diagnostic cycle if continuous diagnostics has not been enabled in
// the meantime. If a breach is processed during startup, before
// startContinuousDiagnostics
// has run, this shutdown would otherwise permanently stop the continuously-registered
// emitters once continuous profiling starts. The lock ensures the check-and-stop cannot
// interleave with a concurrent startContinuousDiagnostics.
synchronized (continuousLifecycleLock) {
if (continuous.get()) {
logger.debug("Continuous diagnostics is active; leaving diagnostic cycle running");
} else {
logger.debug("Shutting down diagnostic cycle");
endDiagnosticCycle();
}
}
} catch (RuntimeException e) {
logger.error("Failed to shutdown cleanly", e);
} finally {
Expand All @@ -127,7 +205,7 @@ private void scheduleEmittingAlertBreachEvent(AlertBreach alert, long end) {
executorService.schedule(
() -> {
try {
emitInfo(alert, cgroupBasePath);
emitInfo(alert);
} catch (RuntimeException e) {
logger.error("Failed to emit breach", e);
}
Expand All @@ -136,16 +214,17 @@ private void scheduleEmittingAlertBreachEvent(AlertBreach alert, long end) {
TimeUnit.SECONDS);
}

private static void emitInfo(AlertBreach alert, Path cgroupBasePath) {
// visible for testing
protected void emitInfo(AlertBreach alert) {
logger.debug("Emitting Code Optimizer Diagnostic Event");
emitAlertBreachJfrEvent(alert);
CodeOptimizerDiagnosticsJfrInit.emitCGroupData(cgroupBasePath);
emitMachineStats();
emitMachineInfo();
}

private static void emitMachineStats() {
MachineStats machineStats = SystemStatsProvider.getMachineStats();
machineStats.commit();
private static void emitMachineInfo() {
MachineInfo machineInfo = SystemStatsProvider.getMachineInfo();
machineInfo.commit();
}

private static void emitAlertBreachJfrEvent(AlertBreach alert) {
Expand Down

This file was deleted.

This file was deleted.

Loading