Metric AggregatorStore optimizations for sorting tags (#2805)

This commit is contained in:
Utkarsh Umesan Pillai 2022-02-02 12:45:03 -08:00 committed by GitHub
parent 840b24e85f
commit c1c5436023
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 196 deletions

View File

@ -12,6 +12,10 @@
thread.
([2844](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2844))
* Performance improvement: when emitting metrics, users are strongly advised to
provide tags with same Key order, to achieve maximum performance.
([2805](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2805/files))
## 1.2.0-rc1
Released 2021-Nov-29

View File

@ -25,14 +25,12 @@ namespace OpenTelemetry.Metrics
{
internal sealed class AggregatorStore
{
private static readonly ObjectArrayEqualityComparer ObjectArrayComparer = new ObjectArrayEqualityComparer();
private readonly object lockZeroTags = new object();
private readonly HashSet<string> tagKeysInteresting;
private readonly int tagsKeysInterestingCount;
// Two-Level lookup. TagKeys x [ TagValues x Metrics ]
private readonly ConcurrentDictionary<string[], ConcurrentDictionary<object[], int>> keyValue2MetricAggs =
new ConcurrentDictionary<string[], ConcurrentDictionary<object[], int>>(new StringArrayEqualityComparer());
private readonly ConcurrentDictionary<Tags, int> tagsToMetricPointIndexDictionary =
new ConcurrentDictionary<Tags, int>();
private readonly AggregationTemporality temporality;
private readonly string name;
@ -178,28 +176,36 @@ namespace OpenTelemetry.Metrics
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int LookupAggregatorStore(string[] tagKeys, object[] tagValues, int length)
{
int aggregatorIndex;
string[] seqKey = null;
var givenTags = new Tags(tagKeys, tagValues);
// GetOrAdd by TagKeys at 1st Level of 2-level dictionary structure.
// Get back a Dictionary of [ Values x Metrics[] ].
if (!this.keyValue2MetricAggs.TryGetValue(tagKeys, out var value2metrics))
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out var aggregatorIndex))
{
if (length > 1)
{
// Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage.
seqKey = new string[length];
tagKeys.CopyTo(seqKey, 0);
// Create a new array for the sorted Tag keys.
var sortedTagKeys = new string[length];
tagKeys.CopyTo(sortedTagKeys, 0);
value2metrics = new ConcurrentDictionary<object[], int>(ObjectArrayComparer);
if (!this.keyValue2MetricAggs.TryAdd(seqKey, value2metrics))
{
this.keyValue2MetricAggs.TryGetValue(seqKey, out value2metrics);
}
}
// Create a new array for the sorted Tag values.
var sortedTagValues = new object[length];
tagValues.CopyTo(sortedTagValues, 0);
// GetOrAdd by TagValues at 2st Level of 2-level dictionary structure.
// Get back Metrics[].
if (!value2metrics.TryGetValue(tagValues, out aggregatorIndex))
Array.Sort(sortedTagKeys, sortedTagValues);
var sortedTags = new Tags(sortedTagKeys, sortedTagValues);
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex))
{
// Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage.
var givenKeys = new string[length];
tagKeys.CopyTo(givenKeys, 0);
var givenValues = new object[length];
tagValues.CopyTo(givenValues, 0);
givenTags = new Tags(givenKeys, givenValues);
aggregatorIndex = this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
{
@ -210,12 +216,12 @@ namespace OpenTelemetry.Metrics
return -1;
}
lock (value2metrics)
lock (this.tagsToMetricPointIndexDictionary)
{
// check again after acquiring lock.
if (!value2metrics.TryGetValue(tagValues, out aggregatorIndex))
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex))
{
aggregatorIndex = Interlocked.Increment(ref this.metricPointIndex);
aggregatorIndex = ++this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
{
// sorry! out of data points.
@ -225,24 +231,68 @@ namespace OpenTelemetry.Metrics
return -1;
}
// Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage.
if (seqKey == null)
{
seqKey = new string[length];
tagKeys.CopyTo(seqKey, 0);
}
var seqVal = new object[length];
tagValues.CopyTo(seqVal, 0);
ref var metricPoint = ref this.metricPoints[aggregatorIndex];
var dt = DateTimeOffset.UtcNow;
metricPoint = new MetricPoint(this.aggType, dt, seqKey, seqVal, this.histogramBounds);
metricPoint = new MetricPoint(this.aggType, dt, sortedTags.Keys, sortedTags.Values, this.histogramBounds);
// Add to dictionary *after* initializing MetricPoint
// as other threads can start writing to the
// MetricPoint, if dictionary entry found.
value2metrics.TryAdd(seqVal, aggregatorIndex);
// Add the sorted order along with the given order of tags
this.tagsToMetricPointIndexDictionary.TryAdd(sortedTags, aggregatorIndex);
this.tagsToMetricPointIndexDictionary.TryAdd(givenTags, aggregatorIndex);
}
}
}
}
else
{
// Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage.
var givenKeys = new string[length];
var givenValues = new object[length];
tagKeys.CopyTo(givenKeys, 0);
tagValues.CopyTo(givenValues, 0);
givenTags = new Tags(givenKeys, givenValues);
aggregatorIndex = this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
{
// sorry! out of data points.
// TODO: Once we support cleanup of
// unused points (typically with delta)
// we can re-claim them here.
return -1;
}
lock (this.tagsToMetricPointIndexDictionary)
{
// check again after acquiring lock.
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out aggregatorIndex))
{
aggregatorIndex = ++this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
{
// sorry! out of data points.
// TODO: Once we support cleanup of
// unused points (typically with delta)
// we can re-claim them here.
return -1;
}
ref var metricPoint = ref this.metricPoints[aggregatorIndex];
var dt = DateTimeOffset.UtcNow;
metricPoint = new MetricPoint(this.aggType, dt, givenTags.Keys, givenTags.Values, this.histogramBounds);
// Add to dictionary *after* initializing MetricPoint
// as other threads can start writing to the
// MetricPoint, if dictionary entry found.
// givenTags will always be sorted when tags length == 1
this.tagsToMetricPointIndexDictionary.TryAdd(givenTags, aggregatorIndex);
}
}
}
}
@ -355,11 +405,6 @@ namespace OpenTelemetry.Metrics
storage.SplitToKeysAndValues(tags, tagLength, out var tagKeys, out var tagValues);
if (tagLength > 1)
{
Array.Sort(tagKeys, tagValues);
}
return this.LookupAggregatorStore(tagKeys, tagValues, tagLength);
}
@ -388,11 +433,6 @@ namespace OpenTelemetry.Metrics
return 0;
}
if (actualLength > 1)
{
Array.Sort(tagKeys, tagValues);
}
return this.LookupAggregatorStore(tagKeys, tagValues, actualLength);
}
}

View File

@ -1,68 +0,0 @@
// <copyright file="ObjectArrayEqualityComparer.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// 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.
// </copyright>
using System.Collections.Generic;
namespace OpenTelemetry.Metrics
{
internal class ObjectArrayEqualityComparer : IEqualityComparer<object[]>
{
public bool Equals(object[] obj1, object[] obj2)
{
if (ReferenceEquals(obj1, obj2))
{
return true;
}
if (ReferenceEquals(obj1, null) || ReferenceEquals(obj2, null))
{
return false;
}
var len1 = obj1.Length;
if (len1 != obj2.Length)
{
return false;
}
for (int i = 0; i < len1; i++)
{
if (!obj1[i].Equals(obj2[i]))
{
return false;
}
}
return true;
}
public int GetHashCode(object[] objs)
{
int hash = 17;
unchecked
{
for (int i = 0; i < objs.Length; i++)
{
hash = (hash * 31) + objs[i].GetHashCode();
}
}
return hash;
}
}
}

View File

@ -1,69 +0,0 @@
// <copyright file="StringArrayEqualityComparer.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// 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.
// </copyright>
using System;
using System.Collections.Generic;
namespace OpenTelemetry.Metrics
{
internal class StringArrayEqualityComparer : IEqualityComparer<string[]>
{
public bool Equals(string[] strings1, string[] strings2)
{
if (ReferenceEquals(strings1, strings2))
{
return true;
}
if (ReferenceEquals(strings1, null) || ReferenceEquals(strings2, null))
{
return false;
}
var len1 = strings1.Length;
if (len1 != strings2.Length)
{
return false;
}
for (int i = 0; i < len1; i++)
{
if (!strings1[i].Equals(strings2[i], StringComparison.Ordinal))
{
return false;
}
}
return true;
}
public int GetHashCode(string[] strings)
{
int hash = 17;
unchecked
{
for (int i = 0; i < strings.Length; i++)
{
hash = (hash * 31) + strings[i].GetHashCode();
}
}
return hash;
}
}
}

View File

@ -0,0 +1,96 @@
// <copyright file="Tags.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// 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.
// </copyright>
using System;
namespace OpenTelemetry.Metrics
{
internal readonly struct Tags : IEquatable<Tags>
{
public Tags(string[] keys, object[] values)
{
this.Keys = keys;
this.Values = values;
}
public readonly string[] Keys { get; }
public readonly object[] Values { get; }
public static bool operator ==(Tags tag1, Tags tag2) => tag1.Equals(tag2);
public static bool operator !=(Tags tag1, Tags tag2) => !tag1.Equals(tag2);
public readonly override bool Equals(object obj)
{
return obj is Tags other && this.Equals(other);
}
public readonly bool Equals(Tags other)
{
// Equality check for Keys
// Check if the two string[] are equal
var keysLength = this.Keys.Length;
if (keysLength != other.Keys.Length)
{
return false;
}
for (int i = 0; i < keysLength; i++)
{
if (!this.Keys[i].Equals(other.Keys[i], StringComparison.Ordinal))
{
return false;
}
}
// Equality check for Values
// Check if the two object[] are equal
var valuesLength = this.Values.Length;
if (valuesLength != other.Values.Length)
{
return false;
}
for (int i = 0; i < valuesLength; i++)
{
if (!this.Values[i].Equals(other.Values[i]))
{
return false;
}
}
return true;
}
public readonly override int GetHashCode()
{
int hash = 17;
unchecked
{
for (int i = 0; i < this.Keys.Length; i++)
{
hash = (hash * 31) + this.Keys[i].GetHashCode() + this.Values[i].GetHashCode();
}
}
return hash;
}
}
}