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

Tracing POC #2946

Merged
merged 19 commits into from
Mar 16, 2023
Merged

Tracing POC #2946

merged 19 commits into from
Mar 16, 2023

Conversation

nomisRev
Copy link
Member

Discussing some ideas for new features with @raulraja he mentioned tracing for Effect, so I was playing around with some ideas and come up with a small POC.

Since raise already relies on CancellationException to co-operate with the coroutine system we currently pay a performance penalty for raise and the stack trace creation that comes with CancellationException by default. This penalty is not needed, and is stack trace creation is by default also disabled in KotlinX Coroutines.

This PR solves the issue of debugging/tracing, and the performance penalty we currently pay for raise.
We can now disable the stack trace creation, and we can ad-hoc enable it for any Effect you compose traced upon or any Raise program you run within a traced block.

public fun main() {
  val error = effect<String, Int> { raise("error") }
  error.traced { traced -> traced.printStackTrace() }
    .fold({ require(it == "error") }, { error("impossible") })
}
arrow.core.continuations.RaiseCancellationException: Raised Continuation
  at arrow.core.continuations.DefaultRaise.raise(Fold.kt:77)
  at MainKtKt$main$error$1.invoke(MainKt.kt:6)
  at MainKtKt$main$error$1.invoke(MainKt.kt:6)
  at arrow.core.continuations.Raise$DefaultImpls.bind(Shift.kt:22)
  at arrow.core.continuations.DefaultRaise.bind(Fold.kt:74)
  at arrow.core.continuations.Effect__TracingKt$traced$2.invoke(Tracing.kt:46)
  at arrow.core.continuations.Effect__TracingKt$traced$2.invoke(Tracing.kt:46)
  at arrow.core.continuations.Effect__FoldKt.fold(Fold.kt:92)
  at arrow.core.continuations.Effect.fold(Unknown Source)
  at MainKtKt.main(MainKt.kt:8)
  at MainKtKt.main(MainKt.kt)

Notes

  • Since the Effect runtime is shared amongst DSL such as either, result, option, ior, etc the traced operator can be added, or used for all these DSLs, and types. Including any of the DSL methods that result in shifting such as ensure, ensureNotNull, bind, etc.

  • Currently only JVM is disabling stack-trace generation. Not sure if we also need to disable it for JS, or Native. (KotlinX Coroutines only disables it for JVM)

  • What API would we want on Traced<R>? Currently we include mentions to ShiftCancellationException and the dynamic call to DefaultShift#shift which is not part of the user stack trace. Should we slightly change the API such that this is improved?

@nomisRev nomisRev added help wanted discussion 1.2.0 Tickets belonging to 1.1.2 labels Feb 23, 2023
@nomisRev nomisRev mentioned this pull request Feb 23, 2023
@kyay10
Copy link
Collaborator

kyay10 commented Mar 12, 2023

Is there a reason to distinguish between RaiseCancellationExceptionNoTrace and RaiseCancellationException on a type level? it seems to me that a val isTraced might be sufficient, although I see that this might be an unnecessary overhead. Instead, would an interface for RaiseCancellationException make sense? That is, to remove the duplication in the RaiseCancellationException.raised and RaiseCancellationException.raise properties.

Also, is there a reason as to why Traced needs to exist as a class? It seems sufficient to just pass the needed information to the lambda, and Traced seems to just be delegating its methods to its properties anyways.

@nomisRev
Copy link
Member Author

Thanks for your review and feedback @kyay10 🙏

Is there a reason to distinguish between RaiseCancellationExceptionNoTrace and RaiseCancellationException on a type level? it seems to me that a val isTraced might be sufficient, although I see that this might be an unnecessary overhead.

Okay, so this was an attempt to hide RaiseCancellationExceptionXXX from the public API, but in turn CancellationExceptionNoTrace is public so not sure if that's really better 🤔 Sadly expect/actual cannot be private 😞

Also, is there a reason as to why Traced needs to exist as a class? It seems sufficient to just pass the needed information to the lambda, and Traced seems to just be delegating its methods to its properties anyways.

The reason for Traced was to have a bit more flexibility. Having Trace as an @JvmInline value class is perhaps better. The reason I modelled it like this is because I wanted to encapsulate the Throwable that gives more flexibility for the future.

I would've really liked to sanitise the trace, but I'm not sure if there is a decent way to do this for Native / JS. We could do it on minimum-effort for JVM only, it seems that is also what KotlinX Coroutines is doing.

I've played around with the changes, and made a commit applying your suggestions @kyay10. WDYT? To my surprise RaiseCancellationException didn't become public in the api file.

@kyay10
Copy link
Collaborator

kyay10 commented Mar 12, 2023

LGTM, perhaps just fix the (very minor) typo I commented above.

Should there also be changes to all the comprehension blocks (is that what we still call them btw?) to add a traced block for each e.g. option.traced? Or is that not useful?

@nomisRev
Copy link
Member Author

LGTM, perhaps just fix the (very minor) typo I commented above.

Oh yes, missed that in the PR 👍

Should there also be changes to all the comprehension blocks (is that what we still call them btw?) to add a traced block for each e.g. option.traced? Or is that not useful?

This shouldn't be necessary you can use traced({ option.bind() }) { } and get a trace from it since bind short-circuit through raise. The biggest issue, or downside, is that it traces Raise and has no way of figuring out where the Option was created. So compared to regular exceptions the trace will point to the throws call and not the Throwable#init.

@nomisRev
Copy link
Member Author

@kyay10 actually I think this change is impossible without having two types 🤔 Since fillInStackTrace is called in the init method of Throwable it will always be uninitialised, hence false. There is no way to influence this so it requires two separate classes.

@kyay10
Copy link
Collaborator

kyay10 commented Mar 14, 2023

@nomisRev Ah that's peculiar! Could RaiseCancellationException become an internal sealed interface with tracing and non-tracing impls? Perhaps even with a companion object { operator fun invoke(..., isTraced: Boolean): RaiseCancellationException } that returns the correct impl based on isTraced.
If that's too complex, honestly I'm fine with the minor code duplication in this case. It's a deep implementation detail that likely won't be modified often, so it might be okay to ignore DRY.

@nomisRev
Copy link
Member Author

Ah that's peculiar!

It's standard behavior for the JVM. Initialisation order of https://stackoverflow.com/questions/32490714/order-of-initialization-instantiation-of-class-variables-of-derived-class-and-in.

It's a deep implementation detail that likely won't be modified often, so it might be okay to ignore DRY.

I think it's fine, especially in a library like Arrow. Optimise for the user facing API > optimise for less code in library.

@nomisRev nomisRev marked this pull request as ready for review March 16, 2023 14:26
@kyay10
Copy link
Collaborator

kyay10 commented Mar 16, 2023

Maybe Trace could be made a value class? I think the other commit (the one that failed the tests) had it as such.

Copy link
Member

@serras serras left a comment

Choose a reason for hiding this comment

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

Looks good to me :)

Comment on lines +171 to +172
this is RaiseCancellationExceptionNoTrace && this.raise === raise -> raised as R
this is RaiseCancellationException && this.raise === raise -> raised as R
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
this is RaiseCancellationExceptionNoTrace && this.raise === raise -> raised as R
this is RaiseCancellationException && this.raise === raise -> raised as R
this.raise === raise && (this is RaiseCancellationExceptionNoTrace || this is RaiseCancellationException) -> raised as R

Copy link
Collaborator

Choose a reason for hiding this comment

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

This change introduces a compilation error because raise and raised properties don't exist in CancellationException. Those properties belong to RaiseCancellationExceptionNoTrace and RaiseCancellationException, so we need the smart cast here.

Copy link
Member

@raulraja raulraja left a comment

Choose a reason for hiding this comment

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

❤️

Co-authored-by: Alejandro Serrano <trupill@gmail.com>
@nomisRev
Copy link
Member Author

Thanks for the great suggestions and review @serras 🙏

@arrow-kt arrow-kt deleted a comment from rafaparadela Mar 16, 2023
…ise/Trace.kt

Co-authored-by: Youssef Shoaib <canonballt@gmail.com>
@nomisRev
Copy link
Member Author

Thanks for fixing the build @franciscodr

Comment on lines +3573 to +3585
public static final synthetic fun box-impl (Ljava/util/concurrent/CancellationException;)Larrow/core/raise/Trace;
public static fun constructor-impl (Ljava/util/concurrent/CancellationException;)Ljava/util/concurrent/CancellationException;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Ljava/util/concurrent/CancellationException;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Ljava/util/concurrent/CancellationException;Ljava/util/concurrent/CancellationException;)Z
public fun hashCode ()I
public static fun hashCode-impl (Ljava/util/concurrent/CancellationException;)I
public static final fun printStackTrace-impl (Ljava/util/concurrent/CancellationException;)V
public static final fun stackTraceToString-impl (Ljava/util/concurrent/CancellationException;)Ljava/lang/String;
public static final fun suppressedExceptions-impl (Ljava/util/concurrent/CancellationException;)Ljava/util/List;
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Ljava/util/concurrent/CancellationException;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Ljava/util/concurrent/CancellationException;
Copy link
Member Author

Choose a reason for hiding this comment

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

Wow, binary says BOOM due to value class 🤯 Never realised this before

nomisRev and others added 2 commits March 16, 2023 21:05
…ise/Trace.kt

Co-authored-by: Youssef Shoaib <canonballt@gmail.com>
@nomisRev nomisRev merged commit 0a86ec2 into main Mar 16, 2023
@nomisRev nomisRev deleted the sv-tracing branch March 16, 2023 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.2.0 Tickets belonging to 1.1.2 discussion help wanted
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants