Skip to content

Commit

Permalink
Added StubLabelledOperationTimer to StubMetricSystem (Consensys#7625)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucassaldanha authored Oct 25, 2023
1 parent fa84b50 commit 9fad727
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright Consensys Software Inc., 2023
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.infrastructure.metrics;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.function.Supplier;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer.TimingContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class StubLabelledOperationTimerTest {

private StubLabelledOperationTimer labelledOperationTimer;
private Supplier<Long> timeProvider;

@BeforeEach
@SuppressWarnings("unchecked")
public void setUp() {
timeProvider = mock(Supplier.class);
when(timeProvider.get()).thenReturn(System.currentTimeMillis());

labelledOperationTimer =
new StubLabelledOperationTimer(
TekuMetricCategory.VALIDATOR, "test_name", "help msg", timeProvider);
}

@Test
public void labelledStubOperationTimerMustMarkTimeCorrectly() {
when(timeProvider.get()).thenReturn(1L, 2L);

final TimingContext timingContext = labelledOperationTimer.labels("foo", "bar").startTimer();
timingContext.stopTimer();

assertThat(labelledOperationTimer.getAverageDuration("foo", "bar")).hasValue(1.0);
}

@Test
public void shouldSupportMultipleDurationsWithinSameLabels() {
when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L);

final OperationTimer timer1 = labelledOperationTimer.labels("foo", "bar");
final OperationTimer timer2 = labelledOperationTimer.labels("foo", "bar");

final TimingContext timingContext1 = timer1.startTimer();
final TimingContext timingContext2 = timer2.startTimer();

timingContext1.stopTimer();
timingContext2.stopTimer();

assertThat(labelledOperationTimer.getDurations("foo", "bar")).contains(5L, 10L);
}

@Test
public void shouldSupportMultipleTimersWithDifferentLabels() {
when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L);

final OperationTimer timer1 = labelledOperationTimer.labels("foo", "bar");
final OperationTimer timer2 = labelledOperationTimer.labels("blip", "blop");

final TimingContext timingContext1 = timer1.startTimer();
final TimingContext timingContext2 = timer2.startTimer();

timingContext1.stopTimer();
timingContext2.stopTimer();

assertThat(labelledOperationTimer.getDurations("foo", "bar")).contains(5L);
assertThat(labelledOperationTimer.getDurations("blip", "blop")).contains(10L);
}

@Test
public void shouldFailGettingTimeOfNonExistingTimer() {
assertThatThrownBy(() -> labelledOperationTimer.getAverageDuration("nope"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
public void shouldReturnExistingTimerForLabels() {
final OperationTimer timer1 = labelledOperationTimer.labels("foo", "bar");
final OperationTimer timer2 = labelledOperationTimer.labels("foo", "bar");

assertThat(timer1).isSameAs(timer2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright Consensys Software Inc., 2023
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.infrastructure.metrics;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.function.Supplier;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer.TimingContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class StubOperationTimerTest {

private StubOperationTimer operationTimer;
private Supplier<Long> timeProvider;

@BeforeEach
@SuppressWarnings("unchecked")
public void setUp() {
timeProvider = mock(Supplier.class);
when(timeProvider.get()).thenReturn(System.currentTimeMillis());

operationTimer =
new StubOperationTimer(
TekuMetricCategory.VALIDATOR, "test_timer", "help msg", timeProvider);
}

@Test
public void shouldCalculateCorrectDuration() {
// duration 6 - 1 = 5
when(timeProvider.get()).thenReturn(1L, 6L);

TimingContext timingContext = operationTimer.startTimer();

assertThat(timingContext.stopTimer()).isEqualTo(5);
}

@Test
public void shouldCalculateMultipleDurations() {
// 1st duration 6 - 1 = 5
// 2nd duration 11 - 1 = 10
when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L);

TimingContext timingContext1 = operationTimer.startTimer();
TimingContext timingContext2 = operationTimer.startTimer();

assertThat(timingContext1.stopTimer()).isEqualTo(5);
assertThat(timingContext2.stopTimer()).isEqualTo(10);
}

@Test
public void shouldCalculateCorrectDurationAverage() {
// 1st duration 6 - 1 = 5
// 2nd duration 11 - 1 = 10
when(timeProvider.get()).thenReturn(1L, 1L, 6L, 11L);

TimingContext timingContext1 = operationTimer.startTimer();
TimingContext timingContext2 = operationTimer.startTimer();

timingContext1.stopTimer();
timingContext2.stopTimer();

assertThat(operationTimer.getAverageDuration()).hasValue(7.5);
}

@Test
public void shouldNotCalculateDurationIfTimerIsNotStopped() {
// We never stop this timer
operationTimer.startTimer();

assertThat(operationTimer.getDurations()).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Consensys Software Inc., 2023
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.infrastructure.metrics;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.function.Supplier;
import org.assertj.core.util.VisibleForTesting;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;

public class StubLabelledOperationTimer extends StubMetric
implements LabelledMetric<OperationTimer> {

private final Map<List<String>, StubOperationTimer> timers = new HashMap<>();
private final Supplier<Long> timeProvider;

protected StubLabelledOperationTimer(
final MetricCategory category, final String name, final String help) {
super(category, name, help);
this.timeProvider = System::currentTimeMillis;
}

@VisibleForTesting
StubLabelledOperationTimer(
final MetricCategory category,
final String name,
final String help,
Supplier<Long> timeProvider) {
super(category, name, help);
this.timeProvider = timeProvider;
}

@Override
public OperationTimer labels(final String... labels) {
return timers.computeIfAbsent(
List.of(labels),
__ -> new StubOperationTimer(getCategory(), getName(), getHelp(), timeProvider));
}

/**
* Return the average time of operations marked by the timer.
*
* @param labels the labels matching the timer
* @return the average time of operations marked by the timer.
* @throws IllegalArgumentException if the provided labels do not correspond to an existing timer.
*/
public OptionalDouble getAverageDuration(final String... labels) {
final StubOperationTimer operationTimer = timers.get(List.of(labels));
if (operationTimer == null) {
throw new IllegalArgumentException("Attempting to get time from a non-existing timer");
}
return operationTimer.getAverageDuration();
}

public Set<Long> getDurations(final String... labels) {
final StubOperationTimer operationTimer = timers.get(List.of(labels));
if (operationTimer == null) {
throw new IllegalArgumentException("Attempting to get time from a non-existing timer");
}
return operationTimer.getDurations();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class StubMetricsSystem implements MetricsSystem {
private final Map<MetricCategory, Map<String, StubGauge>> gauges = new ConcurrentHashMap<>();
private final Map<MetricCategory, Map<String, StubLabelledGauge>> labelledGauges =
new ConcurrentHashMap<>();
private final Map<MetricCategory, Map<String, StubLabelledOperationTimer>>
labelledOperationTimers = new ConcurrentHashMap<>();

private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*");
private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
Expand Down Expand Up @@ -85,7 +87,9 @@ public LabelledMetric<OperationTimer> createLabelledTimer(
final String... labelNames) {
validateMetricName(name);
validateLabelName(labelNames);
throw new UnsupportedOperationException("Timers not supported");
return labelledOperationTimers
.computeIfAbsent(category, __ -> new ConcurrentHashMap<>())
.computeIfAbsent(name, __ -> new StubLabelledOperationTimer(category, name, help));
}

public StubGauge getGauge(final MetricCategory category, final String name) {
Expand All @@ -111,6 +115,15 @@ public StubCounter getCounter(final MetricCategory category, final String name)
() -> new IllegalArgumentException("Unknown counter: " + category + " " + name));
}

public StubLabelledOperationTimer getLabelledOperationTimer(
final MetricCategory category, final String name) {
validateMetricName(name);
return Optional.ofNullable(labelledOperationTimers.get(category))
.map(categoryTimers -> categoryTimers.get(name))
.orElseThrow(
() -> new IllegalArgumentException("Unknown labelled timer: " + category + " " + name));
}

private void validateMetricName(String metricName) {
if (!METRIC_NAME_PATTERN.matcher(metricName).matches()) {
throw new IllegalArgumentException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Consensys Software Inc., 2023
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.infrastructure.metrics;

import java.util.HashSet;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.OptionalLong;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;

public class StubOperationTimer extends StubMetric implements OperationTimer {

private final Random idGenerator = new Random();
private final Supplier<Long> timeProvider;
private final Map<Long, Long> durations = new ConcurrentHashMap<>();

StubOperationTimer(
final MetricCategory category,
final String name,
final String help,
final Supplier<Long> timeProvider) {
super(category, name, help);
this.timeProvider = timeProvider;
}

@Override
public TimingContext startTimer() {
return new StubTimingContext(timeProvider.get());
}

public OptionalDouble getAverageDuration() {
return durations.values().stream().mapToLong(v -> v).average();
}

public Set<Long> getDurations() {
return new HashSet<>(durations.values());
}

public OptionalLong getDurationsByTimingContextId(final Long id) {
final Long maybeDuration = durations.get(id);
return maybeDuration == null ? OptionalLong.empty() : OptionalLong.of(maybeDuration);
}

private class StubTimingContext implements TimingContext {

private final long id = idGenerator.nextLong();
private final long startTime;

public StubTimingContext(final long startTime) {
this.startTime = startTime;
}

@Override
public double stopTimer() {
final long duration = timeProvider.get() - startTime;
durations.put(id, duration);
return duration;
}
}
}
Loading

0 comments on commit 9fad727

Please sign in to comment.