-
Notifications
You must be signed in to change notification settings - Fork 772
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
Metric AggregatorStore optimization for sorting Tag keys #2777
Metric AggregatorStore optimization for sorting Tag keys #2777
Conversation
Codecov Report
@@ Coverage Diff @@
## main #2777 +/- ##
==========================================
+ Coverage 83.75% 83.93% +0.18%
==========================================
Files 251 252 +1
Lines 8864 8902 +38
==========================================
+ Hits 7424 7472 +48
+ Misses 1440 1430 -10
|
// Two-Level lookup. TagKeys x [ TagValues x Metrics ] | ||
private readonly ConcurrentDictionary<string[], ConcurrentDictionary<object[], int>> keyValue2MetricAggs = | ||
new ConcurrentDictionary<string[], ConcurrentDictionary<object[], int>>(new StringArrayEqualityComparer()); | ||
new ConcurrentDictionary<string[], ConcurrentDictionary<object[], int>>(StringArrayComparer); |
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.
another alternate option which doesn't have the risk of too many entries (when user keeps providing keys in different order)
Have the dictionary as before
If tagKeys lookup fail, sort and lookup again.
If fails, insert both original tagKeys and its sorted one to the dictionary.
So that we only store atmost 2 entries per key set. And we only do a single lookup in hotpath, as opposed to 2 look ups.
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.
That's a good suggestion. The only issue with this would be if the user provides the sorted combination as the very first combination and uses some random combination later on. In this case, we would always be sorting the keys.
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, if the same order is re-used, then you get max performance. else lower perf.
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 agree this is good optimization to try out. Reusing the same order is probably the most common scenario. I suppose it's possible for library to use a single instrument in multiple code paths which add dimensions in a different order, but probably an edge case.
A different order may be likely in the event I have two libraries emitting the same metric name, but since they're different libraries they'd be separate Metric instances, right?
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.
Based on offline sync, this may not be feasible.
We can come back to this and keep optimizing. For now, this PR avoids sorting in hot path, and makes a very significant perf boost.
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.
A different order may be likely in the event I have two libraries emitting the same metric name, but since they're different libraries they'd be separate Metric instances, right?
They cannot emit with same metric name (unless different Meter). So it'll be different instances, yes.
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.
Based on offline sync, this may not be feasible.
I was wondering about this myself. Was the issue that synchronizing the two inserts (sorted and original order) would be tough?
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.
The problem here is in keeping the two entries for a given combination in sync.
For example:
counter.Add(10, new("A",1), new("C",3), new("B",2));
Here, since, ("A","C","B") is not present in the dictionary, we add these two entries:
("A","C","B") -> (1,3,2) -> MetricPointIndex1
("A","B","C") -> (1,2,3) -> MetricPointIndex1
Now, if we encounter a
counter.Add(10, new("A",10), new("B",20), new("C",30));
we will add another entry to the inner dictionary:
("A","B","C") -> (1,2,3) -> MetricPointIndex1 -> (10,20,30)-> MetricPointIndex2
This is fine but we also need to add this entry to ("A","C","B"):
("A","C","B") -> (1,3,2) -> MetricPointIndex1 -> (10,30,20)-> MetricPointIndex2 (This is the difficult part)
When we get a new set of tag values we have to find if there is another tag key combination present in the dictionary, and if it's present we have to add the same MetricPointIndex
for the tag values sorted according to the tag keys.
In this case, we get the new tag values (10,20,30) which are sorted by ("A","B","C"). Now we have to find if there is some other combination of ("A","B","C") present in the dictionary. If it's present we then have to sort the tag values according to the combination that is present, in this case, ("A","C","B") which would mean we have to sort the tag values like this: (10,30,20). We then have to ensure that we assign it the same MetricPoint
index.
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.
Ugh... right gotta deal with the values as well.
counterLong.Add(10, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); | ||
counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); | ||
meterProvider.ForceFlush(MaxTimeToAllowForFlush); | ||
sumReceived = GetLongSum(exportedItems); |
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 that going to really tell if we exported one MetricPoint or more than one? IIRC, this method simply sums up all metric points, so won't really validate what you are after..
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 think we should validate that only one Metric is received, and that metric has a single MetricPoint, with the tags (key1,key2,key3).
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.
Ahh sorry my bad. I thought this GetLongSum
would only get the first MetricPoint
's sum for some reason and if the sum value matches the expected value, it would mean that all of Counter.Add
statements contributed to only one MetricPoint
.
I'll update this.
…/github.com/utpilla/opentelemetry-dotnet into utpilla/Metric-AggregatorStore-Optimization
We would also need to have a mapping for sorted tag values. This adds another dictionary lookup. Here are the benchmark results with these changes:
|
} | ||
} | ||
|
||
if (!this.tagValueCombinations.TryGetValue(tagValues, out sortedTagValues)) |
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.
These new dictionaries may grow quite large. Could we do our check of maxMetricPoints
earlier to avoid allowing the dictionaries to grow unbounded?
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.
That check won't help as we are storing different possible combinations of the tag keys. So if we have maxMetricPoints
as 1
we should still allow for multiple entries as we have to account for different combinations. There is no way to know what the maximum possible combinations would be as it depends on the number of keys provided.
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.
Ah right I wasn't thinking about this correctly, so then maybe do the sort as you're doing, but defer this.tagValueCombinations.TryAdd(seqValue, sortedTagValues);
until you know you've gotten a metric point.
private readonly ConcurrentDictionary<string[], string[]> tagKeyCombinations = | ||
new ConcurrentDictionary<string[], string[]>(StringArrayComparer); | ||
|
||
private readonly ConcurrentDictionary<object[], object[]> tagValueCombinations = |
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 think this should be maintained per tagKeyCombination.
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 crossed my mind too, but I considered something like
counter.Add(10, new("A",1), new("C",3), new("B",2));
counter.Add(10, new("D",1), new("E",3), new("F",2));
I think this would actually work fine as is - resulting in [1,3,2] inserted once, but it does seem a little goofy. So, maybe makes sense to scope it to tagKeyCombination for clarity.
The TagKeys and TagValues have to be sorted together and the dictionary that would contain the mapping will have to have both the keys and the values as a composite Here are the benchmarks results with this change:
|
Closing this PR in favor of #2805 |
Fixes item 1 of #2374
Changes
ConcurrentDictionary<string, string>
namedtagKeyCombinations
to map unsorted tag keys combination to the sorted combinationMetricPoint[]
indexBenchmark Results
There is almost a 50% perf improvement starting more than one Tag keys
// * Summary *
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.101
[Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
DefaultJob : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
Without the change:
With the change: