-
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.ActivityLink/ActivityEvent: Enumeration API #68056
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. |
IIRC there was some discussion of a way to make this cheaper in the api review on monday, it might be worth looking up the video on youtube to see if it affects this proposal. |
@CodeBlanch before we proceed with this either the APIs or try to optimize the internal implementation, I would like to see if there is a big concern with the overall performance for this part considering the whole exportation operation and not just this part of enumeration. If we see this is significant, then we can consider it. Do we have such data today to tell? For the proposal, I would prefer optimizing the implementation instead of producing those APIs. If we really need the APIs, I will have the new APIs return I'll mark this issue for the future till we decide about it. |
Tagging subscribers to this area: @dotnet/area-system-diagnostics-activity Issue DetailsBackground and motivationFollow-up to #67207 ActivityEvent and ActivityLink both expose Enumerating using this API is expensive relative to the Enumerate* API on
Today the OpenTelemetry SDK uses reflection to bind to the underlying struct enumerator. It doesn't really need to do that, because it could instead perform a cast (eg
The reason for the slowdown is due to how the For comparison, enumerating over an array... private struct Enumerator
{
private readonly KeyValuePair<string, object>[] source;
private int index;
public Enumerator(KeyValuePair<string, object>[] source)
{
this.source = source;
this.index = -1;
}
public readonly KeyValuePair<string, object> Current => this.source[this.index];
public readonly Enumerator GetEnumerator() => this;
public bool MoveNext()
{
return ++this.index < this.source.Length;
}
}
The goal of this API proposal is to:
API ProposalProposing to use a similar API as #67207: namespace System.Diagnostics
{
partial struct ActivityLink
{
public Enumerator<KeyValuePair<string, object?>> EnumerateTags();
public struct Enumerator<T>
{
private Enumerator(...) { ... }
public readonly Enumerator<T> GetEnumerator() => this;
public readonly ref T Current => ...
public bool MoveNext() { ... }
}
}
partial struct ActivityEvent
{
public Enumerator<KeyValuePair<string, object?>> EnumerateTags();
public struct Enumerator<T>
{
private Enumerator(...) { ... }
public readonly Enumerator<T> GetEnumerator() => this;
public readonly ref T Current => ...
public bool MoveNext() { ... }
}
}
} API Usageforeach (ref readonly ActivityLink activityLink in activity.EnumerateLinks())
{
ExportData(in activityLink);
foreach (ref readonly KeyValuePair<string, object?> tag in activityLink.EnumerateTags())
{
ExportData(in activityLink, in tag);
}
} Alternative DesignsIf we improve the enumerator inside RisksNone /cc @cijothomas @reyang @tarekgh @noahfalk
|
@tarekgh No problem, I expected nothing less 😄 I made these changes so I could test the perf: https://github.com/dotnet/runtime/compare/main...CodeBlanch:activity-detail-enumeration?expand=1 Using stock IEnumerable<KeyValuePair<string, object>> ActivityEvent.Tags:
Using ActivityEvent.Tags cast as ActivityTagsCollection:
Using the reflection engine:
Using the proposed API + changes in that branch:
I have either 1 event w/ 6 tags or 5 events w/ 6 tags each. Is this a real world scenario? SDK adds an event for exceptions so 1 event is ~common. Some users attach ILogger logs to current Activity as events. For those users they will have way more events with tags on them for structured log data (state/scopes). |
Thanks @CodeBlanch. Looking at the numbers, speed wise I am not seeing significant effect. The memory allocation even when using |
@tarekgh A couple more benchmarks for you. May or may not convince you this is worthwhile but I figured it was worth a shot! So the above benchmarks are all for a single Activity/Span. That would be the case of reentrant exporter writing to something like ETW or perhaps a function which only does one thing and then spins down. That is one scenario but most users are probably doing a batch export. Activity/Span instances are held in memory until either a max limit is reached or some time threshold is reached. Here are how the numbers look for a batch size of 2048. Users may set their own size and time limits. Using stock IEnumerable<KeyValuePair<string, object>> ActivityEvent.Tags:
Using the proposed API + changes in that branch:
The enumeration time and memory adds up 😄 A batch of sufficient size could probably survive into gen 1? Not sure about that. |
+1 Typically telemetry data is sent in batches to the backend, and the exporters will take a batch of Activities and process them in a tight loop. For large batch (e.g. some services sending large pile of summary data every 15 minutes) this could be significant. |
I doubt this can happen. All enumeration allocation is small and truly short lived. Such allocations will either be collected or temporarily survive the GC collection if currently in use but not going to migrated to gen 1.
Running operations for 15 minutes can do all kind of allocations and most likely GC collection will kick off anyway during such time. What I am trying to get to what is the percentage of the Events/Links enumeration allocation of the total allocations during the batch operation. This number can help tell if it is worth doing something for it or not. |
100% of the allocations shown above (96 KB & 480 KB) are from The second benchmark using the proposed |
Are we saying the whole batch operation is allocating 14 and 57 bytes if we exclude the Event enumeration? I was expecting much more allocation should be happening there. Do we have any other places in the SDK that reflect on the internal types? or these are the last cases? I am asking to know if we need to collect all cases in one proposal. I was expecting to have this proposal as part of the last approved proposal of the Activity tags/events/links enumeration. |
Yes! Kind of amazing 😄 It is zero-allocation other than the enumerator(s).
Just double-checked, these are the last spots. |
The proposal looks good to me now after using Activity.Enumerator. I am marking this issue as ready for review. |
Looks good as proposed namespace System.Diagnostics;
partial struct ActivityLink
{
public Activity.Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects();
}
partial struct ActivityEvent
{
public Activity.Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects();
} |
Background and motivation
Follow-up to #67207
ActivityEvent and ActivityLink both expose
public IEnumerable<KeyValuePair<string, object?>>? Tags { get; }
for accessing the tags associated with the event or link.Enumerating using this API is expensive relative to the Enumerate* API on
Activity
itself:Today the OpenTelemetry SDK uses reflection to bind to the underlying struct enumerator. It doesn't really need to do that, because it could instead perform a cast (eg
foreach (KeyValuePair<string, object> tag in (ActivityTagsCollection)activityEvents.Tags
) but the underlying struct enumerator is also very slow:The reason for the slowdown is due to how the
List<KeyValuePair<string, object?>>.Enumerator
is being wrapped. Lots of copies ofKeyValuePair<string, object?>
being made in the loop and a copy of the underlying enumerator in ctor.List<T>.Enumerator
also does versioning, which isn't really needed during the OpenTelemetry export phase.For comparison, enumerating over an array...
The goal of this API proposal is to:
API Proposal
Proposing to use a similar API as #67207:
API Usage
Alternative Designs
If we improve the enumerator inside
ActivityTagsCollection
it probably isn't a big deal to live with the cast.KeyValuePair<string, object?>
is not a large struct so returning byreadonly ref
isn't doing much to improve the perf. However during API review of #67207 we moved away from forcing the use of a cast so I thought the proposed API would be preferrable.Risks
None
/cc @cijothomas @reyang @tarekgh @noahfalk
The text was updated successfully, but these errors were encountered: