Skip to content

Commit

Permalink
Make MethodProbes use MethodHandles over reflection (HZ-3024) (#2…
Browse files Browse the repository at this point in the history
…5279)

When accessing the data provided by `MethodProbe`s, currently core Java
reflection is used.

I've replaced this with
[`MethodHandle`s](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/invoke/MethodHandle.html):

- with a `MethodHandle` it's possible to directly access a primitive,
without boxing/unboxing - reducing object creation (and therefore
garbage collection overhead)
- it allows the handle to be constructed once and used over and over
again allowing it to be more efficient

**Benchmarks:**
I benchmarked
([`MethodProbeBenchmark`](hazelcast/internal-benchmarks#40))
the improvement made to a the usage of a `MethodProbe`:

| Access Type | Method Type | ns/op | GC Alloc Rate (MB/s) | GC Alloc
Rate Norm (b/op) | GC Count | GC Time (ms) |
| ------------- | ------------- | ------------- | ------------- |
------------- | ------------- | ------------- |
| Reflection | `long` primitive | 6.8 | 3368 | 24 | 56 | 25 |
| `MethodHandle` | `long` primitive | 4.3 | 0.05 | 0 | 0 | 0 |
| Reflection | `Long` Object | 6.7 | 3419 | 24 | 62 | 23 |
| `MethodHandle` | `Long` Object | 2.1 | 0.05 | 0 | 0 | 0 |

The improvements are:
- faster execution time
- an order of magnitude more efficient in terms of garbage collection

This benchmark includes the overhead of the method operation itself - so
is reflective of the real-world observable performance improvement,
rather than a theoretical comparison of the two implementations.

More information on the implementation can be found in the Javadoc of
[`MethodProbe`](https://github.com/hazelcast/hazelcast/blob/a00165c95bea4641977ae2eaf58aadd5ac42a779/hazelcast/src/main/java/com/hazelcast/internal/metrics/impl/MethodProbe.java).

**Summary of Changes**

- Made `MethodProbe` use `MethodHandle`s
- Migrated static `int` constants in `ProbeUtils` to `ProbeType` `enum`
  - Allows properties to be added
  - Allows static methods to be moved into the object
  - Tidy `flatten`ing logic to avoid a `List.contains` iteration
- Updated `FieldProbe` to suit refactoring

Fixes [#25244](#25244)
  • Loading branch information
JackPGreen committed Oct 3, 2023
1 parent b25844e commit 273eddc
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 233 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.hazelcast.internal.metrics;

/**
* A {@link ProbeFunction} that provides a double value and can be used to
* A {@link ProbeFunction} that provides a {@link double} value and can be used to
* create a probe using {@link MetricsRegistry#registerStaticProbe(Object, String, ProbeLevel, LongProbeFunction)}
*
* @param <S> the type of the source object.
Expand All @@ -26,8 +26,6 @@
public interface DoubleProbeFunction<S> extends ProbeFunction {

/**
* Gets the current value of the source object.
*
* @param source the source object.
* @return the current value of the source object.
* @throws Exception if something fails while getting the value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.hazelcast.internal.metrics;

/**
* A {@link ProbeFunction} that provides a long value and can be used to create
* A {@link ProbeFunction} that provides a {@link long} value and can be used to create
* a probe using {@link MetricsRegistry#registerStaticProbe(Object, String, ProbeLevel, LongProbeFunction)}
*
* @param <S> the type of the source object.
Expand All @@ -26,8 +26,6 @@
public interface LongProbeFunction<S> extends ProbeFunction {

/**
* Gets the current value of the source object as a long.
*
* @param source the source object.
* @return the current value of the source object.
* @throws Exception if something fails while getting the value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package com.hazelcast.internal.metrics.impl;

import static com.hazelcast.internal.metrics.impl.ProbeType.getType;
import static java.lang.String.format;

import com.hazelcast.internal.metrics.DoubleProbeFunction;
import com.hazelcast.internal.metrics.LongProbeFunction;
import com.hazelcast.internal.metrics.MetricDescriptor;
Expand All @@ -28,30 +31,18 @@
import java.util.Map;
import java.util.concurrent.Semaphore;

import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_COLLECTION;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_COUNTER;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_DOUBLE_NUMBER;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_DOUBLE_PRIMITIVE;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_LONG_NUMBER;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_MAP;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_PRIMITIVE_LONG;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.TYPE_SEMAPHORE;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.getType;
import static com.hazelcast.internal.metrics.impl.ProbeUtils.isDouble;
import static java.lang.String.format;

/**
* A FieldProbe is a {@link ProbeFunction} that reads out a field that is annotated with {@link Probe}.
*/
abstract class FieldProbe implements ProbeFunction {

final CachedProbe probe;
final Field field;
final int type;
final ProbeType type;
final SourceMetadata sourceMetadata;
final String probeName;

FieldProbe(Field field, Probe probe, int type, SourceMetadata sourceMetadata) {
FieldProbe(Field field, Probe probe, ProbeType type, SourceMetadata sourceMetadata) {
this.field = field;
this.probe = new CachedProbe(probe);
this.type = type;
Expand Down Expand Up @@ -79,28 +70,30 @@ String getProbeName() {
}

static <S> FieldProbe createFieldProbe(Field field, Probe probe, SourceMetadata sourceMetadata) {
int type = getType(field.getType());
if (type == -1) {
ProbeType type = getType(field.getType());
if (type == null) {
throw new IllegalArgumentException(format("@Probe field '%s' is of an unhandled type", field));
}

if (isDouble(type)) {
if (type.getMapsTo() == double.class) {
return new DoubleFieldProbe<S>(field, probe, type, sourceMetadata);
} else {
} else if (type.getMapsTo() == long.class) {
return new LongFieldProbe<S>(field, probe, type, sourceMetadata);
} else {
throw new IllegalArgumentException(type.toString());
}
}

static class LongFieldProbe<S> extends FieldProbe implements LongProbeFunction<S> {

LongFieldProbe(Field field, Probe probe, int type, SourceMetadata sourceMetadata) {
LongFieldProbe(Field field, Probe probe, ProbeType type, SourceMetadata sourceMetadata) {
super(field, probe, type, sourceMetadata);
}

@Override
public long get(S source) throws Exception {
switch (type) {
case TYPE_PRIMITIVE_LONG:
case TYPE_LONG_PRIMITIVE:
return field.getLong(source);
case TYPE_LONG_NUMBER:
Number longNumber = (Number) field.get(source);
Expand All @@ -125,7 +118,7 @@ public long get(S source) throws Exception {

static class DoubleFieldProbe<S> extends FieldProbe implements DoubleProbeFunction<S> {

DoubleFieldProbe(Field field, Probe probe, int type, SourceMetadata sourceMetadata) {
DoubleFieldProbe(Field field, Probe probe, ProbeType type, SourceMetadata sourceMetadata) {
super(field, probe, type, sourceMetadata);
}

Expand Down

0 comments on commit 273eddc

Please sign in to comment.