-
Notifications
You must be signed in to change notification settings - Fork 1.4k
#9638 Make Cause.toString stack-safe #9853
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job! Just some super minor comments but otherwise looks good to me!
PS: I also benchmarked this approach against the default toString
implementation and it's ~3x faster. Well done!
/** | ||
* Stack-safe toString for Cause | ||
*/ | ||
def causeToString[E](cause: Cause[E]): String = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDUT about adding this within the abstract class and make it final private
(and removing the cause: Cause[E]
argument ofc)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea moving the method to companion Object was to avoid using Cause successors within abstract class. IMO it's more correct. But in fact successors are already mentioned within Cause so let me move it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
Co-authored-by: kyri-petrou <67301607+kyri-petrou@users.noreply.github.com>
if (visitStack.isEmpty) { | ||
results.pop() | ||
} else { | ||
visitStack.pop() match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can actually avoid the either entirely by just matching on Function1
or Cause
- though I am not if it matters since these seem unlikely to retain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
visitRecursive() | ||
} | ||
|
||
final override def toString = causeToString |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kyri-petrou What do you think about avoiding the stack allocations if we just have Die or Fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point (also for Empty
and Interrupt
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've remade it: moved the function to trait and applied it to all "composite" causes
visitRecursive() | ||
} | ||
|
||
final override def toString = causeToString |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point (also for Empty
and Interrupt
)
visitStack.push(Left(() => results.push(s"Stackless(${results.pop()},$stackless)")), Right(cause)) | ||
case Die(value, trace) => | ||
results.push(s"Die($value,$trace)") | ||
case Empty => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I missed this previously. Avoid using case Empty
because it uses the equals
method of Cause which can be expensive. This is much more performant as it relies on type-checking alone:
case Empty => | |
case _: Empty.type => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remade all checks
/** | ||
* A Cause that contains one or more sub-causes | ||
*/ | ||
sealed trait CompositeCause[+E] { self: Cause[E] => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the problem with this approach is that users can now match on this - increasing the API we have to support for binary compatibility. I think we should unfortunately write:
def tostring = self match {
case _: Empty => "Empty"
case _:Die | _:Fail _ => super.toString // not sure if this one is correct
case default => compositToString
@kyri-petrou what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think super.toString
will work because the compiler implements the toString
method of case classes, it's not due to some super class.
In either way, I think we should go with the simplest solution possible. The toString
method of Cause
shouldn't be getting used normally (it's not used in logging, only in ZIO#debug
which shouldn't be used in production code).
I know I was initially on-board with this change, but if this optimization requires code duplication we can revert the last commit and take the allocation hit because it should only really be used during development / debugging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hearnadam if you agree I can make as propsed: revert last commit and apply only change that avoids using Either in stack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make the trait private[Cause]
? that should be okay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@kyri-petrou could you please review/merge PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
No description provided.