com.netflix.spectator.atlas.AtlasTimer Maven / Gradle / Ivy
/*
* Copyright 2014-2023 Netflix, Inc.
*
* 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.netflix.spectator.atlas;
import com.netflix.spectator.api.Clock;
import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.Statistic;
import com.netflix.spectator.api.Timer;
import com.netflix.spectator.impl.StepDouble;
import com.netflix.spectator.impl.StepLong;
import com.netflix.spectator.impl.StepValue;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* Timer that reports four measurements to Atlas:
*
*
* - count: counter incremented each time record is called
* - totalTime: counter incremented by the recorded amount
* - totalOfSquares: counter incremented by the recorded amount2
* - max: maximum recorded amount
*
*
* Having an explicit {@code totalTime} and {@code count} on the backend
* can be used to calculate an accurate average for an arbitrary grouping. The
* {@code totalOfSquares} is used for computing a standard deviation.
*
* Note that the {@link #count()} and {@link #totalTime()} will report
* the values since the last complete interval rather than the total for the
* life of the process.
*/
class AtlasTimer extends AtlasMeter implements Timer {
private final StepLong count;
private final StepDouble total;
private final StepDouble totalOfSquares;
private final StepLong max;
private final Id[] stats;
/** Create a new instance. */
AtlasTimer(Id id, Clock clock, long ttl, long step) {
super(id, clock, ttl);
this.count = new StepLong(0L, clock, step);
this.total = new StepDouble(0.0, clock, step);
this.totalOfSquares = new StepDouble(0.0, clock, step);
this.max = new StepLong(0L, clock, step);
this.stats = new Id[] {
id.withTags(DsType.rate, Statistic.count),
id.withTags(DsType.rate, Statistic.totalTime),
id.withTags(DsType.rate, Statistic.totalOfSquares),
id.withTags(DsType.gauge, Statistic.max)
};
}
@Override void measure(long now, MeasurementConsumer consumer) {
reportMeasurement(now, consumer, stats[0], count, 1.0);
reportMeasurement(now, consumer, stats[1], total, 1e-9);
reportMeasurement(now, consumer, stats[2], totalOfSquares, 1e-18);
reportMaxMeasurement(now, consumer, stats[3], max);
}
private void reportMeasurement(long now, MeasurementConsumer consumer, Id mid, StepValue v, double f) {
// poll needs to be called before accessing the timestamp to ensure
// the counters have been rotated if there was no activity in the
// current interval.
double rate = v.pollAsRate(now) * f;
long timestamp = v.timestamp();
consumer.accept(mid, timestamp, rate);
}
private void reportMaxMeasurement(long now, MeasurementConsumer consumer, Id mid, StepLong v) {
// poll needs to be called before accessing the timestamp to ensure
// the counters have been rotated if there was no activity in the
// current interval.
double maxValue = v.poll(now) / 1e9;
long timestamp = v.timestamp();
consumer.accept(mid, timestamp, maxValue);
}
@Override public Clock clock() {
return clock;
}
@Override public void record(long amount, TimeUnit unit) {
long now = clock.wallTime();
count.getCurrent(now).incrementAndGet();
if (amount > 0) {
final long nanos = unit.toNanos(amount);
total.getCurrent(now).addAndGet(nanos);
totalOfSquares.getCurrent(now).addAndGet((double) nanos * nanos);
updateMax(max.getCurrent(now), nanos);
}
updateLastModTime(now);
}
@Override public void record(long[] amounts, int n, TimeUnit unit) {
final int limit = Math.min(Math.max(0, n), amounts.length);
double accumulatedTotal = 0.0;
long accumulatedMax = Long.MIN_VALUE;
double accumulatedTotalOfSquares = 0.0;
// accumulate results
for (int i = 0; i < limit; i++) {
final long nanos = unit.toNanos(amounts[i]);
if (nanos > 0) {
accumulatedTotal += nanos;
accumulatedTotalOfSquares += ((double) nanos * nanos);
accumulatedMax = Math.max(nanos, accumulatedMax);
}
}
// issue updates as a batch
final long now = clock.wallTime();
count.getCurrent(now).addAndGet(limit);
total.getCurrent(now).addAndGet(accumulatedTotal);
totalOfSquares.getCurrent(now).addAndGet(accumulatedTotalOfSquares);
updateMax(max.getCurrent(now), accumulatedMax);
updateLastModTime(now);
}
@Override public void record(Duration[] amounts, int n) {
final int limit = Math.min(Math.max(0, n), amounts.length);
double accumulatedTotal = 0.0;
long accumulatedMax = Long.MIN_VALUE;
double accumulatedTotalOfSquares = 0.0;
// accumulate results
for (int i = 0; i < limit; i++) {
final long nanos = amounts[i].toNanos();
if (nanos > 0) {
accumulatedTotal += nanos;
accumulatedTotalOfSquares += ((double) nanos * nanos);
accumulatedMax = Math.max(nanos, accumulatedMax);
}
}
// issue updates as a batch
final long now = clock.wallTime();
count.getCurrent(now).addAndGet(limit);
total.getCurrent(now).addAndGet(accumulatedTotal);
totalOfSquares.getCurrent(now).addAndGet(accumulatedTotalOfSquares);
updateMax(max.getCurrent(now), accumulatedMax);
updateLastModTime(now);
}
private void updateMax(AtomicLong maxValue, long v) {
long p = maxValue.get();
while (v > p && !maxValue.compareAndSet(p, v)) {
p = maxValue.get();
}
}
@Override public long count() {
return count.poll();
}
@Override public long totalTime() {
// Cannot change the return type since this is a public API so the result of this can
// potentially overflow and result in a negative value. This is predominately used for
// unit tests so it is rarely a problem in practice. API can be revisited in 2.0.
return (long) total.poll();
}
@Override public BatchUpdater batchUpdater(int batchSize) {
AtlasTimerBatchUpdater updater = new AtlasTimerBatchUpdater(batchSize);
updater.accept(() -> this);
return updater;
}
/**
* Helper to allow the batch updater to directly update the individual stats.
*/
void update(long count, double total, double totalOfSquares, long max) {
long now = clock.wallTime();
this.count.getCurrent(now).addAndGet(count);
this.total.getCurrent(now).addAndGet(total);
this.totalOfSquares.getCurrent(now).addAndGet(totalOfSquares);
updateMax(this.max.getCurrent(now), max);
}
}