Skip to content

Commit

Permalink
Wiring Framework (#9482)
Browse files Browse the repository at this point in the history
Signed-off-by: Cody Littley <cody@swirldslabs.com>
  • Loading branch information
cody-littley authored and imalygin committed Nov 13, 2023
1 parent 7c0ac63 commit 6913b72
Show file tree
Hide file tree
Showing 58 changed files with 8,753 additions and 218 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,9 @@

package com.swirlds.common.metrics.extensions;

import com.swirlds.base.time.Time;
import com.swirlds.common.metrics.FloatFormats;
import com.swirlds.common.metrics.FunctionGauge;
import com.swirlds.common.metrics.Metrics;
import com.swirlds.common.time.IntegerEpochTime;
import com.swirlds.common.utility.ByteUtils;
import com.swirlds.common.utility.StackTrace;
import com.swirlds.common.utility.throttle.RateLimitedLogger;
import com.swirlds.logging.legacy.LogMarker;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongBinaryOperator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* A utility that measures the fraction of time that is spent in one of two phases. For example, can be used to track
Expand All @@ -40,62 +28,7 @@
* This object must be measured at least once every 34 minutes or else it will overflow and return -1.
* </p>
*/
public class FractionalTimer {
private static final Logger logger = LogManager.getLogger(FractionalTimer.class);

/**
* the initial value of status when the instance is created
*/
private static final int INITIAL_STATUS = -1;

/**
* the value of start time when the metric has overflowed
*/
private static final int OVERFLOW = -1;

/**
* if an error occurs, do not write a log statement more often than this
*/
private static final Duration LOG_PERIOD = Duration.ofMinutes(5);

/**
* An instance that provides the current time
*/
private final IntegerEpochTime time;

/**
* Used to atomically update and reset the time and status
*/
private final AtomicLong accumulator;

/**
* limits the frequency of error log statements
*/
private final RateLimitedLogger errorLogger;

/**
* This lambda is used to enter an active state.
*/
private final LongBinaryOperator activationUpdate;

/**
* This lambda is used to enter an inactive state.
*/
private final LongBinaryOperator deactivationUpdate;

/**
* A constructor where a custom {@link Time} instance could be supplied
*
* @param time provides the current time
*/
public FractionalTimer(@NonNull final Time time) {
this.time = new IntegerEpochTime(time);
this.accumulator = new AtomicLong(ByteUtils.combineInts(this.time.getMicroTime(), INITIAL_STATUS));
this.errorLogger = new RateLimitedLogger(logger, time, LOG_PERIOD);

activationUpdate = (currentState, now) -> statusUpdate(currentState, true, now);
deactivationUpdate = (currentState, now) -> statusUpdate(currentState, false, now);
}
public interface FractionalTimer {

/**
* Registers a {@link FunctionGauge} that tracks the fraction of time that this object has been active (out of
Expand All @@ -106,183 +39,47 @@ public FractionalTimer(@NonNull final Time time) {
* @param name a short name for the {@code Metric}
* @param description a one-sentence description of the {@code Metric}
*/
public void registerMetric(
void registerMetric(
@NonNull final Metrics metrics,
@NonNull final String category,
@NonNull final String name,
@NonNull final String description) {
metrics.getOrCreate(new FunctionGauge.Config<>(category, name, Double.class, this::getAndReset)
.withDescription(description)
.withUnit("fraction")
.withFormat(FloatFormats.FORMAT_1_3));
}
@NonNull final String description);

/**
* Notifies the metric that we are entering an active period.
*
* @param now the current time in microseconds
*/
public void activate(final long now) {
accumulator.accumulateAndGet(now, activationUpdate);
}
void activate(final long now);

/**
* Notifies the metric that we are entering an active period.
*/
public void activate() {
this.activate(time.getMicroTime());
}
void activate();

/**
* Notifies the metric that we are entering an inactive period.
*
* @param now the current time in microseconds
*/
public void deactivate(final long now) {
accumulator.accumulateAndGet(now, deactivationUpdate);
}
void deactivate(final long now);

/**
* Notifies the metric that we are entering an inactive period.
*/
public void deactivate() {
this.deactivate(time.getMicroTime());
}
void deactivate();

/**
* @return the fraction of time that this object has been active, where 0.0 means not at all active, and 1.0 means
* that this object has been 100% active.
*/
public double getActiveFraction() {
final long pair = accumulator.get();
return activeFraction(ByteUtils.extractLeftInt(pair), ByteUtils.extractRightInt(pair));
}
double getActiveFraction();

/**
* Same as {@link #getActiveFraction()} but also resets the metric.
*
* @return the fraction of time that this object has been active, where 0.0 means this object is not at all active,
* and 1.0 means that this object has been is 100% active.
*/
public double getAndReset() {
final long pair = accumulator.getAndUpdate(this::reset);
return activeFraction(ByteUtils.extractLeftInt(pair), ByteUtils.extractRightInt(pair));
}

/**
* Gets the fraction of time this object has been active since the last reset.
*
* @param measurementStart the micro epoch time when the last reset occurred
* @param status the current status of this object and the time spent in the opposite status
* @return the fraction of time that this object has been active, where 0.0 means this object is not at all active,
* and 1.0 means that this object is 100% active, or -1 if the metric has overflowed because it was not reset
*/
private double activeFraction(final int measurementStart, final int status) {
if (measurementStart < 0) {
return OVERFLOW;
}
final int elapsedTime = time.microsElapsed(measurementStart);
if (elapsedTime == 0) {
return 0;
}
final int activeTime;
if (isInactive(status)) {
activeTime = Math.abs(status) - 1;
} else {
activeTime = elapsedTime - (status - 1);
}
return ((double) activeTime) / elapsedTime;
}

/**
* Check if this timer is currently inactive.
*
* @param status the current status of this object
* @return true if this timer is currently inactive
*/
private boolean isInactive(final int status) {
return status < 0;
}

/**
* Update the state of this object. The state is the value stored in an atomic long.
*
* @param currentState the current value of the atomic long. Two integers are packed into this long value. The
* first four bytes represent the timestamp when we initially began the current measurement
* period. The last four bytes represent the total time spent in the opposite status. The
* right four bytes can be positive or negative. If positive, it means that the object is
* currently active. If negative, it means that the object is currently inactive.
* @param isBecomingActive true if the object is currently becoming active, false if it is currently becoming
* inactive
* @param now the current time in microseconds
* @return the new value that will be stored in the atomic long.
*/
private long statusUpdate(final long currentState, final boolean isBecomingActive, final long now) {

// the epoch time when the last reset occurred
final int measurementStart = ByteUtils.extractLeftInt(currentState);
// The current status is represented by the (+ -) sign. The number represents the time spent in the
// opposite status. This is so that its time spent being active/inactive can be deduced whenever the sample is
// taken. If the time spent active is X, and the measurement time is Y, then inactive time is Y-X. Since zero
// is neither positive nor negative, the values are offset by 1
final int currentStatus = ByteUtils.extractRightInt(currentState);

// In order to fit the time into 4 bytes, we need to truncate the time to the lower 31 bits.
// This causes the timer to become inaccurate after 34 minutes, but that is not a problem,
// since this utility is intended to be used to measure time over much shorter intervals.
final int truncatedTime = (int) now;

if ((isBecomingActive && !isInactive(currentStatus)) || (!isBecomingActive && isInactive(currentStatus))) {
// this means that the metric has not been updated correctly, we will not change the value
errorLogger.error(
LogMarker.EXCEPTION.getMarker(),
"FractionalTimer has been updated incorrectly. "
+ "Current status: {}, is becoming active: {}, stack trace: \n{}",
currentStatus,
isBecomingActive,
StackTrace.getStackTrace().toString());
return currentState;
}
// the time elapsed since the last reset
final int elapsedTime = IntegerEpochTime.elapsed(measurementStart, truncatedTime);
// the time spent in the opposite status, either active or inactive
final int statusTime = Math.abs(currentStatus) - 1;
// the time spent inactive is all the elapsed time minus the time spent active
// the time spent active is all the elapsed time minus the time spent inactive
final int newTime = elapsedTime - statusTime;
if (newTime < 0 || measurementStart < 0) {
// this is an overflow because the metric was not reset, we are in a state where we can no longer track
// the time spent inactive or active
return ByteUtils.combineInts(OVERFLOW, isBecomingActive ? 1 : -1);
}
if (isBecomingActive) {
// this means this was previously inactive and now started working
return ByteUtils.combineInts(measurementStart, newTime + 1);
}
// this means the object was previously active and now stopped working
return ByteUtils.combineInts(measurementStart, -newTime - 1);
}

/**
* This lambda is used to reset all data stored by this object and begin a new measurement period.
*
* @param currentState the current state of this object
* @return the new state of this object
*/
private long reset(final long currentState) {
return ByteUtils.combineInts(time.getMicroTime(), resetStatus(ByteUtils.extractRightInt(currentState)));
}

/**
* Used to generate the rightmost four bytes in the state during a restart.
*
* @param status the current rightmost four bytes
* @return the new rightmost four bytes
*/
private int resetStatus(final int status) {
if (status > 0) {
return 1;
}
return -1;
}
double getAndReset();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* 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 com.swirlds.common.metrics.extensions;

import com.swirlds.common.metrics.Metrics;
import edu.umd.cs.findbugs.annotations.NonNull;

/**
* A NoOp implementation of {@link FractionalTimer}.
*/
public class NoOpFractionalTimer implements FractionalTimer {

private static final NoOpFractionalTimer INSTANCE = new NoOpFractionalTimer();

private NoOpFractionalTimer() {}

/**
* Get the singleton instance.
*
* @return the singleton instance
*/
public static FractionalTimer getInstance() {
return INSTANCE;
}

/**
* {@inheritDoc}
*/
@Override
public void registerMetric(
@NonNull Metrics metrics, @NonNull String category, @NonNull String name, @NonNull String description) {}

/**
* {@inheritDoc}
*/
@Override
public void activate(final long now) {}

/**
* {@inheritDoc}
*/
@Override
public void activate() {}

/**
* {@inheritDoc}
*/
@Override
public void deactivate(final long now) {}

/**
* {@inheritDoc}
*/
@Override
public void deactivate() {}

/**
* {@inheritDoc}
*/
@Override
public double getActiveFraction() {
return 0;
}

/**
* {@inheritDoc}
*/
@Override
public double getAndReset() {
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class PhaseTimer<T extends Enum<T>> {

if (fractionMetricsEnabled) {
for (final T phase : builder.getPhases()) {
fractionalTimers.put(phase, new FractionalTimer(time));
fractionalTimers.put(phase, new StandardFractionalTimer(time));
}
}

Expand Down
Loading

0 comments on commit 6913b72

Please sign in to comment.