-
Notifications
You must be signed in to change notification settings - Fork 783
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
Revisit the use of generics in Metric instruments #392
Comments
@tarekgh please share you thoughts on this when you are back from vacation. |
I share the same concern for Counter and Gauge protected override Counter<T> CreateCounter<T>(string name, bool monotonic = true)
{
if (typeof(T) != typeof(long) || typeof(T) != typeof(double))
{
throw new InvalidOperationException();
}
// return no op
throw new System.NotImplementedException();
}
protected override Gauge<T> CreateGauge<T>(string name, bool monotonic = true)
{
if (typeof(T) != typeof(long) || typeof(T) != typeof(double))
{
throw new InvalidOperationException();
}
} this does not seem right and I think implementing long and double counters is a much better alternative as it makes users' life better. |
As I mentioned before the JIT is really optimizing that so no perf hit with this pattern. Also, the framework uses this patterns in some places so this is not something new here. In addition to that, the compiler guys are already discussing how to simplify this and I am expecting in the near future compiler versions will allow easier pattern that don't need to do the type checks. At that time we can just update the implementation by then. I don't like to expose a different class for every supported type. in addition the Meter has the methods that explicit returning the desired counter type. so users/developers are not really going to deal with the generic. If in the future we need to support more counter types (integer counters for instance) we'll not have to expose fully new class for that, instead we'll just expose a simple method in the Meter. |
I remember you mentioned JIT optimizes this, and I understand that end users are less impacted as they call However, I faced some issues with use of generics while implementing SDK. I could be implementing these wrong, but i'd like to get opinions on the below: The
The above cannot be compiled. This can only work if I worked around this by doing separate dictionary for long and double as shown below in
This essentially means, we'll have to modify lot of code if we decide to add a new type(int) in the future. |
Your approach looks reasonable but would be nice to tell more details to think if there is a better approach. like, when you'll will cache the counter object? and the dictionary key string is unique for this type of counter or it can be used as a key of different types of counters.
I disagree with that. You are mixing the concept of the public APIs and the implementation details. What you are talking about here is the implementation details. I am concerned more about the public APIs. If we didn't go with the generic approach, we'll have to double the number of the counter classes we expose in addition most of the implementation of these classes will be just a duplication as the functionality is exactly the same and the difference is only in type of the class. why you think the duplication is better? |
If MeterSDK's current approach of keeping separate Dictionary instances for longCounters and doubleCounters is the right way, then when we introduce (if we ever) intCounter, MeterSDK will need to be changed to have this new Dictionary instance. So the implementation still needs to change a lot of places to accommodate new type. (I agree user API won't require change) I am seeing that using generic has not saved any lines of code in the implementation. Implementation is harder whenever I encounter generics. Apart from the examples shared above, how would one use Interlocked classes with generic? Due to the usage of Generics in the API, the SDK is forced to have generics and this is affecting implementation. |
It does save as we'll not introduce a new class and implement it. Also the implementation changes would be very minimal than handling fully a new class.
Where you are going to call the interlock operation? and why? if you are calling it inside the counters, then we already have the type check there which make it easy to know which overload you want to use for the interlock. you are going to do that anyway even if we don't have generics. I mean you'll have to use different interlock overload regardless if having generics or not.
Yes but what is wrong with that? I see your point the generics adding some little overhead in the implementation but I am not really seeing it is a big deal. I am not going to push back more on that, if all agree preferring not having generics, then change the classes to non-generic (by adding at least 6 more classes) but I don't prefer that just because of the implementation details which we are really controlling and not expecting to be complicated either. |
Interlocked will be used in aggregators. (see a sample aggregator https://github.com/open-telemetry/opentelemetry-dotnet/blob/master/src/OpenTelemetry/Metrics/Aggregators/CounterSumAggregator.cs) With generic, I cannot find a way to implement the same. Without generic, For Double, I intend to use the Interlocked.Exchange (https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked.compareexchange?view=netframework-4.8#System_Threading_Interlocked_CompareExchange_System_Double__System_Double_System_Double_) to accumulate sum. I am not sure how to get this with generic T? |
@tarekgh The following is how I write Aggregator without generic. (simplified version) I am yet to find how to implement the same with generic involved here.
And for double:
Agree this is implementation and no API, but the above looks simple to read and reason about. I am comparing it with the below code I have currently with generic. This code needs to be modified to use Interlocked as well, which I am yet to figure out how.
As you can seen, there will be as many code blocks as the number of types for T we allow. (2 currently). So there is no "same code only difference is Type" - which led me to believe generics are not helping here. |
@cijothomas feel free to use non-generic. I have some ideas can achieve implementing it as generic but it has to have the type checking blocks which you don't like. are we talking here about the aggregator classes or you want to convert all counter classes too? I am just checking to know the scope of the change. |
By the way, I may not be able to respond quickly so if you get blocked, don't wait and do the change you think unblock you. thanks for your help. |
For now my goal is to get aggregators implemented correctly - I will move to use non-generics for aggregators for now to unblock and make progress. There may be other areas which might face similar issues. Lets keep this issue open. When you get a chance, please respond on how to achieve the aggregator implementation (shared above) with use of generics. |
Here is one of the ideas how you can implement the generic version: public class CounterSumAggregator<T>
where T : struct
{
private T sum;
private T checkPoint;
public void Checkpoint()
{
this.checkPoint = this.sum;
}
public void Update(T value)
{
if (typeof(T) == typeof(double))
{
double initialTotal, computedTotal;
do
{
initialTotal = (double)(object)this.sum;
computedTotal = initialTotal + (double)(object)value;
}
while (initialTotal != Interlocked.CompareExchange(ref Unsafe.As<T, double>(ref this.sum), computedTotal, initialTotal));
}
else
{
Interlocked.Add(ref Unsafe.As<T, Int64>(ref this.sum), (long)(object)value);
}
}
public T ValueFromLastCheckpoint()
{
return this.checkPoint;
}
} Note, I didn't look at the generated code to see if there will be any perf issue with this approach but I can do that later when I am back. |
Thanks! Let me try the above. |
@cijothomas is there anything blocking you to complete this work? do we still need to discuss using Generics? |
I believe @cijothomas is on PTO, not sure when he's due back. I agreed with @SergeyKanzhelev to pick this up and review what's outstanding once I've finished my OTEP-66 prototype (if Cijo isn't available). I hope to get to this by the end of the week. |
Thanks @MikeGoldsmith for the info. I'll wait then and let me know if there is any issue I can help with. |
@tarekgh I am back from parental and continuing metrics work. You mentioned you haven't checked performance when using Unsafe.As conversions. We definitely want to know if there is a perf hit due to this as this conversions occur for every metric update and is a hot path - Can you help check this? I'd prefer to move away from Generics and implement separate LongCounter, DoubleCounter etc. My reasoning are:
|
@cijothomas to be honest none of the listed reasons really block from using Generic. As I mentioned before Generic will give the benefit not exposing more types. you'll need to expose at least 6 more types with the none generic and in the future, you'll need more if you want to support other types. In addition, as mentioned before, developers are not going to use the Generic directly. I was trying to look at what really is blocking. Here are some more notes just to clarify:
Now, I am not going to push back if you want to go ahead and use non-generic versions. feel free to do so if you believe this better. |
I am in favour replacing generic types with concrete types - although they could wrap the generic type. Generics used in this way are not easy to understand and don't form a user-friendly interface. I feel it also complicates metric processing with multiple I think a mixed approach of concrete and generics could work well. Something along the lines of: internal abstract class MetricBase<T> {}
public class LongMetric : Metric<long> {}
public class DoubleMetric : Metric<double> {} This would allow us to utilise generics and avoid many of the type checks and hopefully simplify code. |
@cijothomas @SergeyKanzhelev feel free to get rid of the generic. I don't think mixing approach will buy us much if we just use non-generic all the time. we'll have to expose other classes anyway. Feel free to close this issue when you do so and let me know if you have any questions. I thought when exposing the nongeneric instances from the Meter was enough but looks this is not the case. |
* Removing more generics for #392 * Fixing the formatting issue * Removed Unsafe everywhere * Fixing method order and file name in the header Co-authored-by: Sergey Kanzhelev <[email protected]>
Not sure if I am allowed to comment but, you may also consider using a union type for If that sounds stupid just ignore me. I am crossing my fingers for years waiting for extension interfaces though. See section An example: Numeric abstraction. |
Parent: #316
With generic metric instruments, the aggregators has to do multiple checks/casts for every metric update as in below example code. JIT may optimize this, but this needs to be discussed to see if it makes sense to get rid of generics.
The above would be re-written as
This would obviously involve copy pasting code for Int64 and Double implementations, but it may be better alternative.
The text was updated successfully, but these errors were encountered: