Skip to content
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

Use native Rust support for async traits in LogExporter::export() method (11% improvement) #2374

Merged
merged 34 commits into from
Dec 20, 2024

Conversation

lalitb
Copy link
Member

@lalitb lalitb commented Dec 2, 2024

Changes

Continuation to #2143, more to show feasibility, performance improvements, and have discussion before raising the PR for eventual review, this PR demonstrate using async traits support in Rust exporter. The support was added in Rust v1.75, while the msrv as of now is v1.70 for otel-sdk

The change is:
Existing:

#[async_trait]
pub trait LogExporter: Send + Sync + Debug {
  async fn export(&mut self, batch: LogBatch<'_>) -> LogResult<()>;
}

PR:

pub trait LogExporter: Send + Sync + Debug {
    fn export<'a>(
        &'a mut self,
        batch: &'a LogBatch<'a>,
    ) -> impl std::future::Future<Output = LogResult<()>> + Send + 'a;
}

There is 11% improvement as seen from stress test:

Before PR:

Number of threads: 16
Throughput: 40,468,186 iterations/sec
Throughput: 40,792,480 iterations/sec
Throughput: 40,456,227 iterations/sec
Throughput: 40,455,461 iterations/sec

After this PR:

Throughput: 44,793,174 iterations/sec
Throughput: 45,321,168 iterations/sec
Throughput: 45,387,969 iterations/sec
Throughput: 45,629,648 iterations/sec
Throughput: 45,607,326 iterations/sec
Throughput: 45,303,331 iterations/sec
Throughput: 45,634,108 iterations/sec

Thanks,
Lalit

Merge requirement checklist

  • CONTRIBUTING guidelines followed
  • Unit tests added/updated (if applicable)
  • Appropriate CHANGELOG.md files updated for non-trivial, user-facing changes
  • Changes in public API reviewed (if applicable)

@lalitb lalitb requested a review from a team as a code owner December 2, 2024 22:34
Copy link

codecov bot commented Dec 6, 2024

Codecov Report

Attention: Patch coverage is 27.07182% with 132 lines in your changes missing coverage. Please review.

Project coverage is 76.7%. Comparing base (80629c8) to head (36ea18a).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
opentelemetry-otlp/src/exporter/http/logs.rs 0.0% 37 Missing ⚠️
opentelemetry-otlp/src/exporter/tonic/logs.rs 0.0% 28 Missing ⚠️
opentelemetry-stdout/src/logs/exporter.rs 0.0% 24 Missing ⚠️
opentelemetry-otlp/src/logs.rs 0.0% 19 Missing ⚠️
opentelemetry-sdk/src/logs/log_processor.rs 74.4% 12 Missing ⚠️
opentelemetry-appender-tracing/src/layer.rs 0.0% 10 Missing ⚠️
opentelemetry-otlp/src/exporter/http/mod.rs 0.0% 1 Missing ⚠️
opentelemetry-otlp/src/exporter/tonic/mod.rs 0.0% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #2374     +/-   ##
=======================================
- Coverage   76.8%   76.7%   -0.2%     
=======================================
  Files        122     122             
  Lines      22217   22266     +49     
=======================================
+ Hits       17082   17093     +11     
- Misses      5135    5173     +38     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@lalitb
Copy link
Member Author

lalitb commented Dec 7, 2024

@lalitb I think it maybe better to first check how to avoid requiring mut reference for exporting, before evaluating the results of this perf test, as that requirement has forced the need of mutex and introduced contention and hence stress tests shows way low throughput than before.

OK, have updated this PR after removing the mut self in #2380. This eliminates the need of mutex for exporter in stress test. The stress result are now similar to earlier i.e., without the inclusion of exporter. Which means, adding futures::block_on() on native async-method doesn't introduce much of latency.

Before this PR (the regression introduced by #2380 because of inclusion of exporter ~44 -> ~40):

Number of threads: 16
Throughput: 40,468,186 iterations/sec
Throughput: 40,792,480 iterations/sec
Throughput: 40,456,227 iterations/sec
Throughput: 40,455,461 iterations/sec

After this PR:

Throughput: 44,793,174 iterations/sec
Throughput: 45,321,168 iterations/sec
Throughput: 45,387,969 iterations/sec
Throughput: 45,629,648 iterations/sec
Throughput: 45,607,326 iterations/sec
Throughput: 45,303,331 iterations/sec
Throughput: 45,634,108 iterations/sec

The cons of introducing the native-async in trait would be

  • Bump the msrv to 1.75
  • The exporter interface is no longer object-safe. Which means, it can't be used in dynamic dispatch (e.g., via dyn Trait). This introduced slight complexity in otel-otlp, where dyn dispatch was used for http and tonic exporters.

Current export method could be simplified a bit for lifetime specifiers. I will look into that. But in general, open for discussion if we should bump the msrv.

@cijothomas cijothomas added this to the 0.28.0 milestone Dec 10, 2024
@cijothomas
Copy link
Member

Bump the msrv to 1.75
The exporter interface is no longer object-safe. Which means, it can't be used in dynamic dispatch (e.g., via dyn Trait). This introduced slight complexity in otel-otlp, where dyn dispatch was used for http and tonic exporters.

Based on today's SIG discussion:
1.75 bump is acceptable.
the exporter interface not being object safe is an easy to fix issue as this PR already demonstrates fixing it for OTLP.

Awaiting more feedback.

@lalitb lalitb mentioned this pull request Dec 12, 2024
4 tasks
@lalitb lalitb changed the title Draft to Discuss: Use native Rust support for async traits in LogExporter::export() method Use native Rust support for async traits in LogExporter::export() method Dec 13, 2024
@lalitb
Copy link
Member Author

lalitb commented Dec 13, 2024

This is ready for review now, with #2417 to bump msrv is merged

@lalitb lalitb added the integration tests Run integration tests label Dec 13, 2024
@lalitb lalitb changed the title Use native Rust support for async traits in LogExporter::export() method Use native Rust support for async traits in LogExporter::export() method (11% improvement) Dec 20, 2024
@lalitb
Copy link
Member Author

lalitb commented Dec 20, 2024

Have updated the description now to reflect the improvement seen through stress test, which is ~11%

@@ -37,14 +39,15 @@ pub struct MockLogProcessor {
impl LogProcessor for MockLogProcessor {
fn emit(&self, record: &mut opentelemetry_sdk::logs::LogRecord, scope: &InstrumentationScope) {
let log_tuple = &[(record as &LogRecord, scope)];
let _ = futures_executor::block_on(self.exporter.export(LogBatch::new(log_tuple)));
let log_batch = LogBatch::new(log_tuple);
let _ = futures_executor::block_on(self.exporter.export(&log_batch));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall flow looks complex..
emit gets a mutable ref to LogRecord. It is ref by design (and mut by design.)
processor, if need to batch it and send, can clone and keep the copy. When it calls the exporter, can it pass ownership the exporter? Wondering why we only pass a ref to the exporter? The processor has no reason to retain ownership anyway, so why not give it up, to the exporter?

Thinking out loud to see if this can be simplified without sacrificing perf..

Copy link
Member Author

@lalitb lalitb Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good point. We are passing LogBatch as reference. LogBatch is a lightweight wrapper around the slice of tuple of reference to LogRecord and InstrumentationScope. Passing it by value won't change the ownership of this borrowed data (record and scope). We can change batch to be passed as value (though record and scope would still remain as reference). This can simplify the design (remove 'a need). However, reference just shows the intent better, and need to test the perf (theoretically it shouldn't affect).

ps - Need to dig the reason for passing record and scope as reference, I remember we discussed and agreed for it in earlier discussion. Here, we are only taking about passing batch as reference or value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The processor has no reason to retain ownership anyway, so why not give it up, to the exporter?

Probably it was not very clear from my above message. Just to clarify - this PR is not changing the ownership model. The exporter always gets the reference to the LogRecord, and it clones if the LogRecord needs to be used beyond the export function lifetime. Are we now discussing the existing design?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reference just shows the intent better,

Not so sure, since we are passing a custom structure LogBatch, which is defined to contains refs only.
Or we can pass slice of refs only, avoiding LogBatch structure entirely. This is better discussed separately.

Are we now discussing the existing design?

This PR is changing existing design - previously owned LogBatch was passed, now we pass a &LogBatch only. It is not clear if that is required for using async native or it is unrelated to that.
If there is no need to change from passing LogBatch to &LogBatch to make async-native work, then lets revert that part.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no need to change from passing LogBatch to &LogBatch to make async-native work, then lets revert that part.

This is reverted now.

stress/src/logs.rs Outdated Show resolved Hide resolved
@@ -81,7 +79,11 @@ pub trait LogExporter: Send + Sync + Debug {
/// A `LogResult<()>`, which is a result type indicating either a successful export (with
/// `Ok(())`) or an error (`Err(LogError)`) if the export operation failed.
///
async fn export(&self, batch: LogBatch<'_>) -> LogResult<()>;
fn export(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a changelog entry as this affects custom exporter authors.

Copy link
Member

@cijothomas cijothomas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

The perf gains are only applicable for concurrently exporting log processors that exports without any batching (like etw exporting).
For components in this repo (like OTLP), export is not in hot path and won't see perf gains.

But irrespective, this still is an improvement and also eliminates one more dependency!

@cijothomas
Copy link
Member

@TommyCpp would you have time to another look?

@cijothomas
Copy link
Member

Merging to unblock other works that'd otherwise have to deal with merge conflicts. Please continue reviewing, and we can address feedbacks in followups.

@cijothomas cijothomas merged commit 23f6ae2 into open-telemetry:main Dec 20, 2024
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration tests Run integration tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants