Fwd: RFC: JFR Recording Context

Ludovic Henry ludovic at datadoghq.com
Fri Nov 12 13:22:43 UTC 2021


Hello,

As it's been a couple of months since my last update, I wanted to share
some of the progress I've made there. The TL;DR is that it now supports
adding context to any event, as well as adding a global or per-event-type
filters to control the generation of events based on the context.

The changes are available at
https://github.com/openjdk/jdk/compare/master...luhenry:jfr-context

# Description of a context

A context is a map of string key and string value which are attached to
relevant JFR events. This context is installed on the current thread, and
can be snapshotted and activated on other threads.

# Building a context

To build a context, you first declare the keys of the context :

```

var contextKey1 = RecordingContextKey.forName("Key1");

```

Then, to use that key to build a specific context, use the following:

```

var context = RecordingContext.where(contextKey1, "Value1").build();

```

# Installing a context

Building a context will also install the context on the current thread. To
uninstall the context, simply do `context.close()`.

To capture the context and restore it on another thread, do:

```

// On the thread where the context is already installed:

var snapshot = RecordingContext.snapshot();

// On the thread you want to restore/install the context:

try (var activation = snapshot.activate()) { ... }

```

You can also use helper functions for structured concurrency:

```

RecordingContext.callWithSnapshot(o -> { ... }, snapshot);

RecordingContext.runWithSnapshot(() -> { ... }, snapshot);

```

# Lifecycle of a context

Let’s take a look at an example:

```

public class Program {

  private static final RecordingContextKey contextKey1 =

    RecordingContextKey.forName("Key1");

  public static void main(String[] args) {

    try (Recording recording = new Recording()) {

      recording.start();

      recording.enable("jdk.ThreadSleep");

      try (RecordingContext context =

            RecordingContext.where(contextKey1, "Value1").build()) {

        ForkJoinPool pool = ForkJoinPool.commonPool();

        final RecordingContext.Snapshot snapshot =
RecordingContext.snapshot();

        pool.execute(() -> {

          try (var activation = snapshot.activate()) {

            Thread.sleep(1);

          }

        });

        pool.execute(() -> {

          try (var activation = snapshot.activate()) {

            Thread.sleep(1);

          }

        });

        pool.shutdown();

        pool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);

      }

      recording.stop();

      recording.dump(Files.createTempFile("my-recording", ".jfr"));

    }

  }

}

```

The two `jdk.ThreadSleep` events generated are as follows:
```

jdk.ThreadSleep {

  startTime = 11:06:20.336 (2021-11-12)

  duration = 1.05 ms

  time = 1.00 ms

  eventThread = "main" (javaThreadId = 1)

  stackTrace = [

    java.lang.Thread.sleep(long)

    Program.lambda$main$0(RecordingContext$Snapshot) line: 23

    java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec() line:
1395

    java.util.concurrent.ForkJoinTask.doExec() line: 373

    java.util.concurrent.ForkJoinPool.externalHelpQuiescePool(long,
boolean) line: 2104

  ]

  context = [

    Key1 -> Value1,

  ]

}

jdk.ThreadSleep {

  startTime = 11:06:20.336 (2021-11-12)

  duration = 1.10 ms

  time = 1.00 ms

  eventThread = "ForkJoinPool.commonPool-worker-1" (javaThreadId = 18)

  stackTrace = [

    java.lang.Thread.sleep(long)

    Program.lambda$main$0(RecordingContext$Snapshot) line: 23

    java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec() line:
1395

    java.util.concurrent.ForkJoinTask.doExec() line: 373

    java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinTask,
ForkJoinPool$WorkQueue) line: 1182

  ]

  context = [

    Key1 -> Value1,

  ]

}

```

# Filtering on the context

In order to control the generation of events, this change introduces
context filters. A filter is per-type or for all types, and is installed
globally.

To build and install a filter, you do the following:

```

var filter = RecordingContextFilter.Config.createFilter()

  .forType(EventType.getEventType(TestEvent.class), b -> {

    b.hasEntry(contextKey1, "Value1");

  })

  .forAllTypes(b -> {

    b.hasKey(contextKey1);

  })

  .build()

RecordingContextFilter.Config.setContextFilter(filter);

```

To uninstall a filter, set it to null:
`RecordingContextFilter.Config.setContextFilter(null);`

The currently supported operations are:

- hasKey(key)

- hasEntry(key, value)

- hasContext()

- hasNoContext()

To guarantee a certain level of performance, I've made two tradeoffs:

 - A filter can't have custom code and can only use the supported
operations, and

 - The filters are eagerly computed on the installation of a context to
avoid matching the filter on the generation of every event. The per-type
and for all types results are stored in a map (or array in native) to
guarantee O(1) lookup time.

>From microbenchmarks, the current overhead is the following (all the JMH
numbers at [1]):

- Generating an event with the @Context annotation and an empty context
adds ~25ns to the generation of the event

- Each entry in the context adds ~100ns to the generation of an event

- Setting a filter does not impact the performance of the generation of an
event

I'll submit a draft PR in the following hours to give an easier time for
reviews and feedback.

Thank you again very much, and looking forward to your feedback and
questions.

Ludovic

[1]

Benchmark                                                Mode  Cnt  Score
Error  Units

RecordingContextBenchmark.Baseline.withContext           avgt   30  0.046 ±
0.001  us/op

RecordingContextBenchmark.Baseline.withoutContext        avgt   30  0.021 ±
0.001  us/op

RecordingContextBenchmark.One.withContext                avgt   30  0.160 ±
0.005  us/op

RecordingContextBenchmark.One.withoutContext             avgt   30  0.021 ±
0.001  us/op

RecordingContextBenchmark.Three.withContext              avgt   30  0.331 ±
0.009  us/op

RecordingContextBenchmark.Three.withoutContext           avgt   30  0.022 ±
0.001  us/op

RecordingContextBenchmark.Two.withContext                avgt   30  0.237 ±
0.003  us/op

RecordingContextBenchmark.Two.withoutContext             avgt   30  0.021 ±
0.001  us/op

RecordingContextFilterBenchmark.Baseline.withContext     avgt   30  0.048 ±
0.001  us/op

RecordingContextFilterBenchmark.Baseline.withoutContext  avgt   30  0.021 ±
0.001  us/op

RecordingContextFilterBenchmark.One.withContext          avgt   30  0.154 ±
0.003  us/op

RecordingContextFilterBenchmark.One.withoutContext       avgt   30  0.023 ±
0.001  us/op

RecordingContextFilterBenchmark.Three.withContext        avgt   30  0.346 ±
0.007  us/op

RecordingContextFilterBenchmark.Three.withoutContext     avgt   30  0.021 ±
0.001  us/op

RecordingContextFilterBenchmark.Two.withContext          avgt   30  0.239 ±
0.004  us/op

RecordingContextFilterBenchmark.Two.withoutContext       avgt   30  0.021 ±
0.001  us/op


More information about the hotspot-jfr-dev mailing list