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

Performance Feature #971

Merged
merged 74 commits into from
Nov 20, 2020
Merged

Performance Feature #971

merged 74 commits into from
Nov 20, 2020

Conversation

maciejwalkowiak
Copy link
Contributor

@maciejwalkowiak maciejwalkowiak commented Oct 8, 2020

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Performance feature implemented according to specs: https://develop.sentry.dev/sdk/unified-api/tracing

The follow-up to this PR is Spring Boot + Sentry Performance integration implemented in separate branch: https://github.com/getsentry/sentry-java/tree/performance-spring

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • All tests passing
  • No breaking changes

🔮 Next steps

Spring Boot + Performance integration.

…new transaction type.

SentryItem is a base class for SentryEvent and upcoming Transaction class.
@codecov-io
Copy link

codecov-io commented Oct 8, 2020

Codecov Report

Merging #971 (a568d39) into main (8e49958) will increase coverage by 0.51%.
The diff coverage is 79.35%.

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #971      +/-   ##
============================================
+ Coverage     72.03%   72.54%   +0.50%     
- Complexity     1338     1476     +138     
============================================
  Files           138      154      +16     
  Lines          4895     5270     +375     
  Branches        499      531      +32     
============================================
+ Hits           3526     3823     +297     
- Misses         1108     1171      +63     
- Partials        261      276      +15     
Impacted Files Coverage Δ Complexity Δ
...ntry/src/main/java/io/sentry/DiagnosticLogger.java 94.44% <ø> (ø) 13.00 <0.00> (ø)
sentry/src/main/java/io/sentry/Dsn.java 94.11% <ø> (ø) 12.00 <0.00> (ø)
sentry/src/main/java/io/sentry/HubAdapter.java 9.43% <0.00%> (-0.78%) 4.00 <0.00> (ø)
sentry/src/main/java/io/sentry/NoOpLogger.java 83.33% <0.00%> (-16.67%) 5.00 <0.00> (ø)
sentry/src/main/java/io/sentry/Sentry.java 43.07% <0.00%> (-1.73%) 18.00 <0.00> (ø)
...entry/src/main/java/io/sentry/SystemOutLogger.java 13.63% <0.00%> (-0.65%) 2.00 <0.00> (ø)
.../java/io/sentry/exception/InvalidDsnException.java 46.15% <ø> (ø) 2.00 <0.00> (?)
...src/main/java/io/sentry/CustomSamplingContext.java 25.00% <25.00%> (ø) 1.00 <1.00> (?)
sentry/src/main/java/io/sentry/NoOpHub.java 53.12% <33.33%> (-2.05%) 17.00 <1.00> (+1.00) ⬇️
sentry/src/main/java/io/sentry/SentryItemType.java 77.77% <42.85%> (-22.23%) 5.00 <2.00> (+2.00) ⬇️
... and 46 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8e49958...a568d39. Read the comment docs.

Copy link
Member

@bruno-garcia bruno-garcia left a comment

Choose a reason for hiding this comment

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

I know it's a draft but I threw some thoughts in hopes to help see possible limitations we might hit later

sentry/src/main/java/io/sentry/GsonSerializer.java Outdated Show resolved Hide resolved
sentry/src/main/java/io/sentry/SentryEvent.java Outdated Show resolved Hide resolved
sentry/src/main/java/io/sentry/SentryEvent.java Outdated Show resolved Hide resolved
sentry/src/main/java/io/sentry/SentryEvent.java Outdated Show resolved Hide resolved
sentry/src/main/java/io/sentry/SentryEnvelopeItem.java Outdated Show resolved Hide resolved
@maciejwalkowiak
Copy link
Contributor Author

Should we have separate event processors for transactions or the idea is to use single type of unit processor for both events and transactions?

@bruno-garcia
Copy link
Member

Should we have separate event processors for transactions or the idea is to use single type of unit processor for both events and transactions?

In JS it seems transactions go through EventProcesor but not BeforeSend. I believe to the surprise of everyone (or an accident even at first) but it's now being used as such.
It's known that folks want to be able to avoid some endpoints/transactions from being captured at all like /health which your LB hits every few seconds.

That said, JS got a new sampling strategy which we could add in v1, but I'd keep out both beforeSend and eventProcessor for now. Hopefully forever if we can, and potentially add other hooks later when we understand better the use cases

@bruno-garcia
Copy link
Member

@maciejwalkowiak
Copy link
Contributor Author

@bruno-garcia if transactions go through the same event processors as normal events - event processors must be based on the SentryBaseEvent from now on. I did not find anywhere in the docs what are the common properties of events and transactions - things like environment, release, dist, user etc. Is there a place I can look it up?

@maciejwalkowiak
Copy link
Contributor Author

maciejwalkowiak commented Oct 23, 2020

At this stage, we have:

  • a class containing common properties for SentryEvent and Transaction
  • Transaction class representing transaction
  • TransactionContexts that inherits from Contexts and adds Trace property - as far as I can see all properties on Contexts can also be applied to TransactionContexts
  • Sentry and Hub have methods to start transaction and capture transaction

The code for sending transactions will look more or less:

val contexts = TransactionContexts()
contexts.trace = Trace()
contexts.trace.op = "http"
contexts.trace.description = "some request"
contexts.trace.status = SpanStatus.OK
contexts.trace.setTag("myTag", "myValue")

val transaction = Sentry.startTransaction(contexts)
transaction.setName("newtranxs5")
transaction.finish()

Sentry.captureTransaction(transaction, null)

Sentry#captureTransaction is not a part of Hub changes but I imagine this is how we want to actually send the transaction..?

What's still left is:

  • Spans - and since transaction is also a Span, Span will likely be an interface
  • Process transactions through event processors - for that we will need to change the signature of event processors to handle SentryBaseEvent
  • Sampling
  • Sentry trace header

@bruno-garcia
Copy link
Member

@bruno-garcia if transactions go through the same event processors as normal events - event processors must be based on the SentryBaseEvent from now on. I did not find anywhere in the docs what are the common properties of events and transactions - things like environment, release, dist, user etc. Is there a place I can look it up?

Let's not run it through EventProcessor for now. We should investigate proper hooks for this.

Sentry#captureTransaction is not a part of Hub changes but I imagine this is how we want to actually send the transaction..?

I believe folks pass a reference to the hub to the transaction so it can flush itself. It comes with downsides like (what if close was called). I don't have a proposal on how to do this so feel free to give it a shot.

private @Nullable Date timestamp;

/** A list of spans within this transaction. Can be empty. */
private final @NotNull List<Span> spans = new ArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

this list is not thread-safe, wondering if this would race or a transaction is always single thread because of thread locals?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We cannot guarantee that Transaction will be used in single thread. What do you think is a better fit here - CopyyOnRewrite or Synchronized variant of the ArrayList?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer CopyOnWriteArrayList, @bruno-garcia usually has strong feelings about thread-safe APIs, lets ask him as well.

Copy link
Member

Choose a reason for hiding this comment

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

The worry isnt' much about guarantee of an instance not being used in multiple threads. If we document Transaction is not thread-safe and therefore should not be used concurrently, that'd be OK.

The issue we have the Scope must be thread-safe. In backend Java when a new thread is spawned, it access the main hub to clone its state. That happens while that main hub might be getting modified.

On Android it's even more relevant given we have a single static, mutable, Hub. There we don't allow for push and pop scope though (they are no-op).

We need to discuss the right data type here for collections to be thread-safe as in not throwing in a foreach because they are modified etc, but also how can we make the APIs atomic. Those that need to be.

I wonder how this works in Python, /cc @untitaker

Copy link
Member

Choose a reason for hiding this comment

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

Appending a span to the list is thread-safe in Python. When iterating over the spans we make sure not to trigger any of those exceptions as we don't pop or replace items in the list, ever.

Python's scopes are somewhat thread-safe but we try to make sure each concurrency unit basically gets its own scope.

Whether you want to support concurrency within a transaction right now is up to you but it's a valid usecase.

@maciejwalkowiak maciejwalkowiak mentioned this pull request Nov 16, 2020
8 tasks
Copy link
Member

@bruno-garcia bruno-garcia left a comment

Choose a reason for hiding this comment

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

I've only made it to 30 files out of 72. At this point I just want to merge this and work on any feedback on next PRs, but if possible to wait until tomorrow I can give feedback on the other 42 files I missed

final StackItem item = stack.peek();
if (item != null) {
sentryId = item.client.captureTransaction(transaction, item.scope, hint);
item.scope.clearTransaction();
Copy link
Member

Choose a reason for hiding this comment

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

+1 on clearing on the finally block. The caller won't know about the error unless peeking at the SDK diag logs.

sentry/src/main/java/io/sentry/ISpan.java Outdated Show resolved Hide resolved
sentry/src/main/java/io/sentry/SentryClient.java Outdated Show resolved Hide resolved
sentry/src/main/java/io/sentry/Scope.java Outdated Show resolved Hide resolved
} else if (item instanceof Session) {
return Session;
} else {
return Attachment;
Copy link
Member

Choose a reason for hiding this comment

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

Attachment should have type json (which isn't visible here but it's the state the envelope item ends up in)

sentry/src/main/java/io/sentry/Span.java Show resolved Hide resolved
* @param throwable - the throwable
* @return span context or {@code null} if no corresponding span context found.
*/
public SpanContext getSpanContext(final @NotNull Throwable throwable) {
Copy link
Member

Choose a reason for hiding this comment

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

H: Just realized that this approach will not work once we flush individual spans (i.e: Don't accumulate all spans in all open transactions on the server, mainly due to the memory pressure but instead flush them out as they close to allow batches to be flushed to the server)

protected @Nullable SpanStatus status;

/** A map or list of tags for this event. Each tag must be less than 200 characters. */
protected @NotNull Map<String, String> tags = new ConcurrentHashMap<>();
Copy link
Member

Choose a reason for hiding this comment

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

This being concurrent makes me wonder to what degree this code was implemented with concurrency in mind. To what I understood so far no of this stuff was designed to be accessed concurrently, right? The tags here only will? Could you please elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was designed with concurrency in mind or at least I wanted it to be concurrent. If I missed certain places we can address them individually.

Copy link
Member

@bruno-garcia bruno-garcia left a comment

Choose a reason for hiding this comment

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

37 files more to go

return response;
} finally {
span.finish();
if (urlTemplate.get().isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

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

is this missing !?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's "if queue in thread local is empty remove the queue from thread local". This is actually copied from Spring Boot that implements similar functionality with Micrometer metrics.

}

// TODO: this method ideally gets extracted or moves to Hub itself
private @Nullable ISpan resolveActiveSpan() {
Copy link
Member

Choose a reason for hiding this comment

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

Agreed. @untitaker how are you doing this in Python?
@HazAT for JS, PHP etc?

Copy link
Member

Choose a reason for hiding this comment

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

We got hub.scope.span in python nowadays

Copy link
Member

Choose a reason for hiding this comment

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

it's bound manually or automatically? Daniel mentioned it's manually bound on JS.
Each time span.startChild is called the scope.span is overwriten?

e);
} finally {
if (item != null) {
item.scope.clearTransaction();
Copy link
Member

Choose a reason for hiding this comment

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

I'm afraid this could cause problem on Android.
Could we get around this somehow? Like we check if the reference in the scope we have is actually the one we're closing, then we can clean it. Because I imagine there could be a second startTransation before this captureTrasnaction is called


public void setTag(String key, String value) {
if (tags == null) {
tags = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

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

This would need to be a ConcurrentMap if it's base type to Scope

@@ -56,10 +56,10 @@ class DirectoryProcessorTest {
val path = getTempEnvelope("envelope-event-attachment.txt")
assertTrue(File(path).exists()) // sanity check
// val session = createSession()
// whenever(fixture.envelopeReader.read(any())).thenReturn(SentryEnvelope.fromSession(fixture.serializer, session, null))
// whenever(fixture.envelopeReader.read(any())).thenReturn(SentryEnvelope.from(fixture.serializer, session, null))
Copy link
Member

Choose a reason for hiding this comment

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

deadcode, delete?

@bruno-garcia bruno-garcia merged commit 3c87a48 into main Nov 20, 2020
@bruno-garcia bruno-garcia deleted the performance branch November 20, 2020 22:48
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants