-
Notifications
You must be signed in to change notification settings - Fork 858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prototype for Exponential Histogram Aggregator #3724
Prototype for Exponential Histogram Aggregator #3724
Conversation
...a/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregator.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, looking really good!
...a/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregator.java
Show resolved
Hide resolved
|
||
class DoubleExponentialHistogramBuckets implements ExponentialHistogramBuckets { | ||
|
||
public static final int MAX_BUCKETS = 320; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC - This is the maximum of JUST positive buckets and we have another 320 for negative?
I think this works out well, because our primary use case (initially) is latency, where we won't have negative buckets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the positive and negative buckets are separate, both instances of this class. So overall the max buckets for a histogram would be 640 plus the zero count.
That reminds me that I should probably lazily-instantiate the positive and negative buckets according to the data. No need to use memory on the negative buckets if there aren't any negative recordings.
...java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java
Outdated
Show resolved
Hide resolved
...in/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramData.java
Outdated
Show resolved
Hide resolved
sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricDataType.java
Show resolved
Hide resolved
import java.util.function.Supplier; | ||
|
||
final class DoubleExponentialHistogramAggregator | ||
extends AbstractAggregator<ExponentialHistogramAccumulation> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an FYI - plan to refactor aggregator to have less... history in it and streamline which methods you actually need/use.
A few comments/thoughts:
- Your merge method will likely need to figure out how to re-scale histograms between recordings. Specifically for stateful / cumulative aggregation, you're likely to have a lower scale in the cumulative than the most recent recordings. If this turns out to be a major performance issue, there are some alternatives we can talk over.
- For delta expert, the merge method is unused, which I assume is how this code is working right now?
- I mention this below, but
Handle
needs to be threadsafe, while all these other method are assumed to only use input values to produce their outputs. For a method like "accumulateDouble" on the aggregator this is only used by async instruments, so I wouldn't worry about optimising it too much. (Creating a temporary handle to accumulate is reasonable).
...java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java
Show resolved
Hide resolved
.../java/io/opentelemetry/sdk/metrics/internal/aggregator/ExponentialHistogramAccumulation.java
Outdated
Show resolved
Hide resolved
sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/WindowedCounterArray.java
Outdated
Show resolved
Hide resolved
@Nonnull | ||
@Override | ||
public List<Long> getBucketCounts() { | ||
// todo LongList optimisation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can get much of the way there by creating an array here and returning Arrays.asList
, LongList
will have a similar API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍
sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/WindowedCounterArray.java
Outdated
Show resolved
Hide resolved
Question for @jsuereth - do you think an accumulation should always remain immutable? I ask because it would likely be more efficient to merge If it should be immutable that's also fine, was just wondering if I should create a new accumulation or merge into an existing one. edit: On second thoughts, I think it's fine because when converted to metric data everything is copied. I was just wary because the accumulations are autovalue which are usually immutable classes. |
3c2040b
to
c2edebc
Compare
@jamesmoessis I think mutating accumulations as a performance optimisation is ok, but we should be strict about when/how and lifecycle/ownership. Specifcally if you look at DeltaMetricStorage, it relies on immutable accumulations right now. We could adapt this to do a clone + mutate approach when merging deltas. Can you open a ticket on that? I can take a crack, or you can feel free to as well. Also, FYI - this PR will break significantly here: #3762 |
@yzhuge Are these algorithms supposed to work with recording the full double range in a single histogram? I am getting some inaccuracies while testing with // scale = -3.
long i = valueToIndex(Double.MAX_VALUE); // i = 128
BigDecimal lowerBound = BigDecimal.valueOf(256).pow( (int) i); // 256^128
assertThat(BigDecimal.valueOf(Double.MAX_VALUE)).isGreaterThanOrEqualTo(lowerBound); // fails. lower bound > max double
// but passes if I set i = 129 The indexing strategy is as shown in This seems inconsistent to me, would you have any idea what this is due to? |
@jamesmoessis "off by one bucket" is expected on log() based methods. When a value is near a boundary, the method may return either buckets on the two sides of the boundary. Double.MAX is close to power of 2, therefore close to a boundary. Thus the off by one error. This is normal on floating point calculation. For zero and negative scales, the ExponentIndexer is completely accurate because it uses only integer operations. ScaledExpIndexer.getMaxIndex(), getMinIndexNormal(), getMinIndex() gives completely accurate theoretical value for min and max indexes at a given scale. For scale -3, Max index is 127. So 128 is OK. |
Codecov Report
@@ Coverage Diff @@
## main #3724 +/- ##
============================================
+ Coverage 89.32% 89.65% +0.32%
- Complexity 4085 4229 +144
============================================
Files 488 505 +17
Lines 12602 13054 +452
Branches 1226 1274 +48
============================================
+ Hits 11257 11703 +446
Misses 925 925
- Partials 420 426 +6
Continue to review full report at Codecov.
|
@yzhuge thankyou for the explanation, that makes sense. For the purposes of keeping this PR simple, I want just the one indexing strategy for now. I will make a note that other indexing strategies are optimal at different scales, and the code here is set up easily so they can be switched out for each other when scale changes. I can address that in a separate PR, along with more rigorous testing. |
…some comments to javadoc
… scale reduction bug
...g/src/main/java/io/opentelemetry/sdk/testing/assertj/metrics/ExponentialHistogramAssert.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comment, but this is looking great for an initial implementation/hook into the SDK! Great work.
@jsuereth is this something we want in the SDK for the next release, or are we waiting for spec stability around it? |
@jkwatson Regarding stability, only the MetricData interface piece matters (which is effectively based on our stable protocol), The rest is hidden in internal and cannot be exposed in the current public API. From that standpoint I think this is ok. @jkwatson / @anuraaga If you have suggestions on how to "hide" or "denote experimental" anything in MetricData, let me know. I'd prefer (if possible) to just put a disclaimer on the class itself (and the enum value). While ExponentialHistogram cannot change in breaking ways in our protocol, we may want room to encode it in a different way. For now it's a pure interface, so I think we may have enough flexibility here, but PTAL at that area. |
@jkwatson @jsuereth This is an alpha module, I think it's always ok to merge if the current state looks fine. @jamesmoessis Can you merge main since it's been a little while? I tried but don't seem to have permission to push to this PR |
@anuraaga I've merged main |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @jamesmoessis - I understand there will be some more test coverage in the future but tried to identify stuff that might not be "pushing the boundaries of the histogram" :) But let me know if you'd rather work later, keeping an eye on https://app.codecov.io/gh/open-telemetry/opentelemetry-java
isNotNull(); | ||
if (actual.getAggregationTemporality() != AggregationTemporality.CUMULATIVE) { | ||
failWithActualExpectedAndMessage( | ||
actual, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to add test cases for this class - should we file an issue for it or is it simple enough now? Or can we delete the class for now if it's not useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone ahead and added tests for it. Codecov seems to be green now.
return (ExponentialHistogramData) getData(); | ||
} | ||
return DoubleExponentialHistogramData.EMPTY; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably easier to add a test case than a TODO
return; | ||
} else if (by < 0) { | ||
logger.warning( | ||
"downScale() expects non-negative integer but was given" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a programming bug in our codebase? Then we can throw IllegalStateException instead. Otherwise probably can add a test case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this should be IllegalStateException.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
We're trying to get Metrics graduated and out of alpha quickly. I think we should start treating new-feature code review different than bug-fix / friction-fix / tuning code reviews (e.g. Jack's cardinality limits) differently going forward for metrics to make the job of releasing easier. I.e. rather than viewing Metrics as alpha (where we can continue to dig holes we can't get out of), view it as "Beta" or "approaching stable". That said my comment around this PR stands. I think it's fine to include, the publicly exposed parts are based on stable protocol and flexible enough we can tweak the implementation going forward. |
I will review this work in depth some time today, if that will help. I have a branch with an OTel-Go implementation of this and I'd like to compare and make notes. |
@@ -92,6 +92,8 @@ private static String cleanMetricName(String descriptorMetricName) { | |||
return Collector.Type.SUMMARY; | |||
case HISTOGRAM: | |||
return Collector.Type.HISTOGRAM; | |||
case EXPONENTIAL_HISTOGRAM: | |||
return Collector.Type.UNKNOWN; // todo exporter for exponential histogram |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an implied question here -- what should an exporter or processor do when it sees this data and only knows about the old style of histogram. The same question comes up (and is even more pressing) for the OTel collector. I prototyped a converted to produce explicit-boundary histograms here: open-telemetry/opentelemetry-collector#3841
For Prometheus, there will be special considerations -- an auto-scaling aggregator is going to create problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting point. Converting to explicit boundary histogram could work. I also wonder how various backends would handle that. I certainly don't have the answers for that.
sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/BucketMapper.java
Show resolved
Hide resolved
return; | ||
} else if (by < 0) { | ||
logger.warning( | ||
"downScale() expects non-negative integer but was given" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this should be IllegalStateException.
@@ -48,6 +45,9 @@ | |||
} | |||
|
|||
public boolean record(double value) { | |||
if (value == 0.0) { | |||
throw new IllegalStateException("Illegal attempted recording of zero at bucket level."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will potentially crash the user's app, correct? Are we really ok with that, rather than ignoring the recording, and using the ThrottlingLogger to make sure we don't spam the logs too hard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's indicative of a bug if this happens. The Handle
ensures that all zero values go towards a separate counter zeroCount
. This avoids Math.log(0)
.
Up to you if we log or throw an exception, I was just aligning with what @anuraaga said here on a similar issue: #3724 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too sure, but one difference between the two is that one is a private method but this one is public.
Perhaps my comment wasn't clear enough, by programming bug I meant a programming bug of the OTel SDK, not user app. If this is a public API that can be called by a user, and it's their problematic code that causes 0.0 to be passed, then it's a case we wouldn't (can't based on our error handling policy) throw an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This brings another issue to mind, this method should be package private, so I've got ahead and made it so. This is not a user facing method. In fact the class itself is package private. If 0 is passed to this, it would mean there's a bug in the SDK.
Given this info, should it be a log or an exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exception is fine. I see this case is guarded by the caller. Can. you add a comment to that effect right before the line where you throw here? Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I've added a comment 👍
Thanks @jmacd feel free to message me on Slack if you ever want to chat about exponential histogram stuff. |
Thought I'd raise a draft PR since this piece of work is becoming quite large, and it would probably be better to have some iterative review on it. I apologise for the size, but it was necessary to get any sort of functionality for testing.
This histogram autoscales according to the recordings it receives. Each histogram starts at scale 20 and downscales if a recording does not fit (if calculated index can't be represented by
int
, or value would cause there to be more buckets than allowed). The max number of buckets is 320. I have taken these values from the NrSketch and the Go implementation done by @jmacd. The unit tests demonstrate this working.The Indexer used is a simple Logarithm mapper as seen in
DoubleExponentialHistogramBuckets.LogarithmMapper
. I haven't done any fancy indexing techniques.The recordings are stored via the
DoubleExponentialHistogramBuckets
which usesNrSketch's circular backing array, theEDIT: changed this to a reference implementationWindowedCounterArray
, whichMultiTypeCounterArray
for variable bit-length. I've taken them directly from NrSketch which is Apache-2.0 so I've retained their copyright notice next to the Opentelemetry one. However the spotlessJava doesn't like this currently so the build fails. These classes need some additional cleanup too.MapCounter
as discussed.For this to come out of draft, there are some todos which I have commented throughout the code:
merge()
which actually aggregates the accumulations together.metrics-testing
and use them for the testsLongList
optimisation ingetBucketCounts()
mentioned previously Data classes for exponential histogram prototype (#3550) #3637