Skip to content

Performance: criticality of code paths

Tommy Ludwig edited this page Jul 21, 2022 · 3 revisions
Note
This wiki is currently a Work-In-Progress.

Performance of Micrometer code is an important concern, but not all code paths have the same criticality to runtime performance. In addition to the time to execute code paths, we must also be conscious of garbage generated.

Highly critical paths

Code paths that are executed in-line with user code directly add to the execution time of user code. Performance of these code paths are of highest criticality, especially if they may be used in short and often executed parts of user code.

In these highly critical paths, sacrifices to code readability may be made where significant performance benefits can be demonstrated.

Recording a measurement

This is the most critical path because it is expected to be called most often in-line with user code.

The code for recording largely depends on the meter implementation, but there is a lot of use of classes from the java.util.concurrent.atomic package. Look at the implementations of the increment method for a Counter or the record method for a Timer/DistributionSummary, e.g. CumulativeCounter / StepTimer.

Retrieving a meter

In many cases, a meter will be retrieved each time to record a measurement. In some cases, a reference to a meter may be retained if there are no dynamic tag values, and this can save the cost of retrieving the meter before each recording.

The Builder API for meters will often be used as a way to retrieve meters. The builder does come with the cost of allocation. For example:

JettySslHandshakeMetrics retrieving meter via Builder
@Override
public void handshakeSucceeded(Event event) {
    SSLSession session = event.getSSLEngine().getSession();
    Counter.builder(METER_NAME)
            .baseUnit(BaseUnits.EVENTS)
            .description(DESCRIPTION)
            .tag(TAG_RESULT, "succeeded") // constant tag
            .tag(TAG_PROTOCOL, session.getProtocol()) // dynamic tag
            .tag(TAG_CIPHER_SUITE, session.getCipherSuite()) // dynamic tag
            .tags(tags)
            .register(registry) // create or retrieve
            .increment(); // record
}

Any tags will involve the Tag and Tags API.

The combination of meter name and tags forms the Meter Id. If the combination of name + tags has not been registered before, the above code would create the Meter. If it had been created before, this would retrieve the previously created Meter.

For the retrieval inside MeterRegistry code, besides getting the meter from the Map that holds meters, all MeterFilter are applied to the Meter or Id parameter to get the mapped ID which is the key for stored meters.

Less critical paths

Some code paths are expected to be executed relatively less frequently. And some code paths are executed separately from user code, which does not directly contribute to user code execution time. They do contribute indirectly since they are part of the same JVM/process. In a resource constrained environment, these code paths can contribute to user code execution time as much as the in-line code paths.

Creating a meter

While each unique meter will need to be created once during the lifetime of the application and often in-line with user code, it is considered less critical than retrieving a meter or recording a measurement. This is because creation only happens once per meter, whereas retrieval and/or recording is expected to happen many times per meter over the lifetime of the application. Additionally, since high cardinality is a concern because a meter per unique meter name + tag combination will be created, cardinality should be kept reasonably bounded and therefore meters created should be far fewer than the number of times recording a measurement or retrieving a meter.

See this code block in MeterRegistry for meter creation logic.

  • An object lock is used to synchronize meter creation.

  • The accept method of each MeterFilter is called on the meter ID to be created. If not accepted, a no-op meter is created instead.

  • If a meter takes a DistributionStatisticConfig, the configure method for all MeterFilter will be called on the config.

  • Synthetic associations are marked.

  • All meterAddedListeners are called on the meter before it is put into the meter Map.

Publishing metrics

Publishing happens separately from user code execution usually in a dedicated single thread pool, which makes it less critical that it has the absolutely highest performance possible. Publishing includes the following code paths:

  • Some registries use MeterPartition to partition meters into batches for publishing.

  • Retrieving all meters via MeterRegistry#getMeters which copies a List of the meters.

  • All NamingConvention API for sanitization and conventionalization of names/values from canonical form.

  • How meters are converted from Micrometer’s in-memory model to a publish format differs by implementation.

  • Depending on registry implementation, sending metric data may use the HttpSender API or a third-party API.