-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[API Proposal]: System.Diagnostics.Activity: Enumeration API #67072
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @tarekgh, @tommcdon, @pjanotti Issue DetailsBackground and motivationOne of the main things the OTel .NET SDK needs to do is export What would be great is if the .NET API exposed a public method for enumerating these elements without allocation and as /cc @reyang @cijothomas API Proposalnamespace System.Diagnostics
{
public class Activity
{
public ReadOnlySpan<KeyValuePair<string, object?>> TagsAsReadOnlySpan();
public ReadOnlySpan<ActivityLink> LinksAsReadOnlySpan();
public ReadOnlySpan<ActivityEvent> EventsAsReadOnlySpan();
}
public class ActivityTagsCollection
{
public ReadOnlySpan<KeyValuePair<string, object?>> AsReadOnlySpan();
}
} API UsageIn some cases we export to DTOs. Other cases we export to some reusable buffer. Worth noting, OTel Export API is not async. Export either runs inline on calling thread ("simple mode") or on a dedicated background thread ("batch mode"). public static Dto ToDto(this Activity activity)
{
var dto = new Dto();
foreach (ref readonly KeyValuePair<string, object?> tag in activity.TagsAsReadOnlySpan())
{
dto.Attributes.Add(CreateDtoTag(in tag));
}
foreach (ref readonly ActivityLink link in activity.LinksAsReadOnlySpan())
{
dto.Links.Add(CreateDtoLink(in link));
}
foreach (ref readonly ActivityEvent @event in activity.EventsAsReadOnlySpan())
{
dto.Events.Add(CreateDtoEvent(in @event));
}
return dto;
}
private static DtoLink CreateDtoLink(in ActivityLink link)
{
vat dto = new DtoLink();
ActivityTagsCollection tags = (ActivityTagsCollection)link.Tags; // Currently IEnumerable<KeyValuePair<string, object?>> so needs a cast. Would be nice to make it concrete type but not strictly required.
foreach (ref readonly KeyValuePair<string, object?> tag in tags.TagsAsReadOnlySpan())
{
dto.Attributes.Add(CreateDtoTag(in tag));
}
return dto;
}
// Not shown CreateDtoTag or CreateDtoEvent but they will work just like the above. Alternative DesignsI did some performance testing. Using public struct Enumerator<T> : IEnumerator<T>
{
private static readonly DiagNode<T> s_Empty = new DiagNode<T>(default!);
private DiagNode<T>? _nextNode;
private DiagNode<T> _currentNode;
public Enumerator(DiagNode<T>? head)
{
_nextNode = head;
_currentNode = s_Empty;
}
public readonly ref T Current => ref _currentNode.Value;
T IEnumerator<T>.Current => Current;
object? IEnumerator.Current => Current;
public bool MoveNext()
{
if (_nextNode == null)
{
_currentNode = s_Empty;
return false;
}
_currentNode = _nextNode;
_nextNode = _nextNode.Next;
return true;
}
public void Reset() => throw new NotSupportedException();
public void Dispose()
{
}
} If the ROS option is not viable I will spend more time flushing this out. RisksNone that I can think of.
|
@CodeBlanch how you think |
Tagging subscribers to this area: @dotnet/area-system-diagnostics-activity Issue DetailsBackground and motivationOne of the main things the OTel .NET SDK needs to do is export What would be great is if the .NET API exposed a public method for enumerating these elements without allocation and as /cc @reyang @cijothomas API Proposalnamespace System.Diagnostics
{
public class Activity
{
public ReadOnlySpan<KeyValuePair<string, object?>> TagsAsReadOnlySpan();
public ReadOnlySpan<ActivityLink> LinksAsReadOnlySpan();
public ReadOnlySpan<ActivityEvent> EventsAsReadOnlySpan();
}
public class ActivityTagsCollection
{
public ReadOnlySpan<KeyValuePair<string, object?>> AsReadOnlySpan();
}
} API UsageIn some cases we export to DTOs. Other cases we export to some reusable buffer. Worth noting, OTel Export API is not async. Export either runs inline on calling thread ("simple mode") or on a dedicated background thread ("batch mode"). public static Dto ToDto(this Activity activity)
{
var dto = new Dto();
foreach (ref readonly KeyValuePair<string, object?> tag in activity.TagsAsReadOnlySpan())
{
dto.Attributes.Add(CreateDtoTag(in tag));
}
foreach (ref readonly ActivityLink link in activity.LinksAsReadOnlySpan())
{
dto.Links.Add(CreateDtoLink(in link));
}
foreach (ref readonly ActivityEvent @event in activity.EventsAsReadOnlySpan())
{
dto.Events.Add(CreateDtoEvent(in @event));
}
return dto;
}
private static DtoLink CreateDtoLink(in ActivityLink link)
{
vat dto = new DtoLink();
ActivityTagsCollection tags = (ActivityTagsCollection)link.Tags; // Currently IEnumerable<KeyValuePair<string, object?>> so needs a cast. Would be nice to make it concrete type but not strictly required.
foreach (ref readonly KeyValuePair<string, object?> tag in tags.TagsAsReadOnlySpan())
{
dto.Attributes.Add(CreateDtoTag(in tag));
}
return dto;
}
// Not shown CreateDtoTag or CreateDtoEvent but they will work just like the above. Alternative DesignsI did some performance testing. Using public struct Enumerator<T> : IEnumerator<T>
{
private static readonly DiagNode<T> s_Empty = new DiagNode<T>(default!);
private DiagNode<T>? _nextNode;
private DiagNode<T> _currentNode;
public Enumerator(DiagNode<T>? head)
{
_nextNode = head;
_currentNode = s_Empty;
}
public readonly ref T Current => ref _currentNode.Value;
T IEnumerator<T>.Current => Current;
object? IEnumerator.Current => Current;
public bool MoveNext()
{
if (_nextNode == null)
{
_currentNode = s_Empty;
return false;
}
_currentNode = _nextNode;
_nextNode = _nextNode.Next;
return true;
}
public void Reset() => throw new NotSupportedException();
public void Dispose()
{
}
} If the ROS option is not viable I will spend more time flushing this out. RisksNone that I can think of.
|
@tarekgh What I did for my testing was implement it like this in public ReadOnlySpan<KeyValuePair<string, object?>> AsReadOnlySpan()
{
return CollectionsMarshal.AsSpan(_list);
}
Performance is really great! ROS is 🔥 But that only works because So if this perf is compelling enough, I think we could switch
|
You have used CollectionsMarshal which exist only in .NET 5.0 and up. This will be challenging if we need to support it for ns2.0. Or we'll have to change the implementation to not use collections and use arrays. I am not sure if this is worth the effort here.
The current implementation of DiagLinkedList is safe enough to be used in the enumeration while the list can change. This will not be guaranteed when using collection (e.g. I think the best here is exposing |
Background and motivation
One of the main things the OTel .NET SDK needs to do is export
Activity
(trace) data. There is a lot of enumeration needed to do that.Activity
tags, links(+tags), and events(+tags). Today the .NET API is allIEnumerable<T>
/IEnumerator<T>
based and all of the items being enumerated arereadonly struct
s. Our performance testing was showing a lot of allocations incurred using the enumeration interfaces. To avoid that we generate IL to bind to the internalstruct
contracts. That fixes the allocations but a lot time is currently being spent copying value types.What would be great is if the .NET API exposed a public method for enumerating these elements without allocation and as
readonly ref
to avoid copying./cc @reyang @cijothomas
API Proposal
API Usage
In some cases we export to DTOs. Other cases we export to some reusable buffer. Worth noting, OTel Export API is not async. Export either runs inline on calling thread ("simple mode") or on a dedicated background thread ("batch mode").
Alternative Designs
I did some performance testing. Using
ReadOnlySpan<T>
had the best perf.ActivityTagsCollection
isList<T>
based, so might not be a challenge to expose ROS there.Activity
versions are using a custom internal linked list. Might be difficult to expose ROS there without changing the underlying data structure. I thought I would shoot for the moon first so I asked for the ROS API but an alternative API would be to expose more traditional structs for enumeration that have areadonly ref T Current
. For example:If the ROS option is not viable I will spend more time flushing this out.
Risks
None that I can think of.
The text was updated successfully, but these errors were encountered: