Nicer output when an exception bubbles through main#4968
Nicer output when an exception bubbles through main#4968asterite merged 12 commits intocrystal-lang:masterfrom asterite:feature/better-callstack
Conversation
There was a problem hiding this comment.
I'd love an env var or other option to get a "low-level" raw stacktrace which shows pretty much the same as what existed before this PR, minus the @@skip check. It'd be useful for debugging on occations.
This is much more necessary given how often the debug info is blatantly wrong (see #4538 for the true scale of this). The symbol info is pretty much impossible to be wrong.
| if file && file != "??" | ||
| next if @@skip.includes?(file) | ||
|
|
||
| # Turn to relative to the current dir, if possible |
There was a problem hiding this comment.
This probably would get confusing if the executable changes it's directory mid-program. We could have multiple stacktraces from the same program have different representations for the same file. I think it'd be best to record Dir.current very early in the init.
There was a problem hiding this comment.
We have ˋProcess::INITIAL_PWDˋ in process/executable_path.cr maybe we can use that? (As a constant its set very early)
There was a problem hiding this comment.
Nice find! I think we can actually expose that constant as it might be useful for others.
There was a problem hiding this comment.
I used it, but I'm not sure now we should expose it. Or maybe find another way/name and document it. So out of the scope of this PR.
| # Turn to relative to the current dir, if possible | ||
| file = file.lchop(current_dir) | ||
|
|
||
| file_line_column = "#{file} #{line}:#{column}" |
There was a problem hiding this comment.
I'd like to have "#{file}:#{line}:#{column}" because my text editor understands when you tell it to open foo.cr:7:4.
There was a problem hiding this comment.
Oh, I thought about changing that to be like that but thought it would be harder on the eyes, and easier to copy just the filename. But yes, if it's easier to open than on an editor, let's change it (Ruby also uses that format)
Some editors understand this format so it's easier to jump to that location.
|
@RX14 Maybe we can keep it all the time. I was thinking maybe like this: What do you think? |
| if function.starts_with?("*raise<") || | ||
| function.starts_with?("*CallStack::") || | ||
| function.starts_with?("*CallStack#") || | ||
| function == "main" |
There was a problem hiding this comment.
If I have a custom method called main it would get hidden from the stacktrace? Just rename foo to main in your example to see what I mean.
There was a problem hiding this comment.
You are right. I'll just keep those entries then.
There was a problem hiding this comment.
I did it because there's only one "main", I don't know why when there's debug info Foo#main just appears as main. But well, it's not a big deal not removing those entries.
There was a problem hiding this comment.
Couldn't you just remove the last entry? This would require to count the size, but I don't think this should be a big concern.
There was a problem hiding this comment.
Or simply break from the loop after __crystal_main is processed.
There was a problem hiding this comment.
No, because en release some traces might not be there. I think it's simpler to just keep things as they are now regarding this.
There was a problem hiding this comment.
Regardless if it might not show up in some scenarios, anything after __crystal_main shouldn't be of interest so I think it should be possible to break when this function is processed.
Though it's not a big deal having a line more at the end.
|
@asterite I've never actually needed to use the function hex locations, they're practically random with ASLR anyway. What I have done before is to use |
|
@RX14 Sounds good. The main issue now is deciding how to trigger this behaviour. We can either use an ENV var, configure something in CallStack itself, or use a compile-time flag. Also, what do you think about removing the |
|
@RX14 Actually, I think we can show the IP address when there's no file/line/column information. That happens when you compile with |
|
Wouldn't "symbol location" be a much less confusing name than "ip address"... Regardless, my entire point with the request was to request a way to switch between showing debug info and plain symbols without recompiling. Sorry if I didn't make it clear. |
ysbaddaden
left a comment
There was a problem hiding this comment.
I suggested a few minor tweaks, otherwise: good job! That really cleans things up.
The symbol address should always be left out as they don't help much the ordinary developer (that includes me), even when the debug info couldn't be found. It's not uncommon (at least on Linux) to have some lines be found in the DWARF sections, while some aren't... we'd have a weirdly mixed backtrace output when that happens for example.
Now, as @RX14 suggested, maybe we could have a flag to disable the pretty output to get the raw backtrace. Maybe just react --no-debug which can be found out with flag?(:debug).
|
|
||
| # Crystal methods (their mangled name) start with `*`, so | ||
| # we remove that to have less clutter in the output. | ||
| function = function.lchop('*') |
There was a problem hiding this comment.
All the unmangling of the function name could happen in the elseif above (line 168-170), when we failed to decode the function name from DWARF and got the mangled symbol instead.
| # CallStack tries to make files relative to the current dir, | ||
| # so we do the same for tests | ||
| current_dir = Dir.current | ||
| current_dir += '/' unless current_dir.ends_with?('/') |
There was a problem hiding this comment.
Maybe use File::SEPARATOR for future-proofing on Windows?
| struct CallStack | ||
| # Compute current directory at the beginning so filenames | ||
| # are always shown relative to the *starting* working directory. | ||
| CURRENT_DIR = Process::INITIAL_PWD + '/' |
There was a problem hiding this comment.
Same here: File::SEPARATOR.
There was a problem hiding this comment.
We really should have a function to get a path relative to another path
https://docs.python.org/3/library/os.path.html#os.path.relpath
There was a problem hiding this comment.
@oprypin I agree, though I don't know if I'd like ../../ to appear in the output. I checked Go for example and they always use absolute paths (though Ruby uses relative paths if the path is in the current directory)
|
@ysbaddaden @RX14 I applied your suggestions. I also made it so that if the env variable Going back to the original example, we have: Without dsymutil: With dsymutil: Without dsymutil, with CRYSTAL_CALLSTACK_FULL_INFO: With dsymutil, with CRYSTAL_CALLSTACK_FULL_INFO: |
|
@asterite I used |
|
@RX14 Updated! Now without dsymutil when full info is requested: With dsymutil when full info is requested: That way you can see the real symbol name plus the address. Is it good now? :-) |
RX14
left a comment
There was a problem hiding this comment.
The implementation looks good! Would be nice to use --no-debug and setting ENV["CRYSTAL_CALLSTACK_FULL_INFO"] = "1" to spec the output in all possible cases though.
I have only a small nitpick-kinda-question actually. 😁 Why 8 spaces of indentation? Been using some shards with some nested path structure and ended with really long lines in the backtrace. Don't see it as a real problem, but always wondered about it and perhaps avoid wrapping at 120 columns like happens under some scenarios. Anyway, thank you for the contribution! ❤️ ❤️ ❤️ |
|
@luislavena I copied Ruby here. I find that those 8 spaces immediately signals an exception was thrown, but only because I'm used to that. I have no problem changing that to 2 spaces, if @RX14 agrees. I'll also add specs for |
I have no problems with the 8 spaces either, was wondering if there was a particular reason that I was missing. Thanks for the details. And thank you for the improvements! |
|
I changed it to use 2 spaces instead of 8. I think that's better because it gives more spaces to show the trace. Thanks for the suggestion, @luislavena ! |
|
@asterite what about the spec changes? |
|
I'd like |
|
An update to this issue - would it be better to add colours in the form of ANSI colour formatting to these errors? I personally think it's a little bit of effort that goes a long way to making errors more readable, and I'm willing to do it - just not sure if everyone supports this idea. |
|
CC @asterite ^ |
When an exception happens in a program and isn't catched, what's printed is a bit hard to digest. For example, for this program:
The output is:
That's at least on Mac without running
dsymutil. On linux, or on Mac when runningdsymutil, this is the output:That's a bit better: we can see
raiseandCallStackare removed from the trace.So I see a few things to improve:
gdborlldb)dsymutilon Mac,CallStackandraiseare still there, and also all names have an annoying*prefix (this is part of the function mangling in Crystal)dsymutilon Mac, filenames are shown as "??" which clutters the outputmainis always in the trace but adds little info because it's always the same and adds no infoThis PR improves all of that:
CallStackandraiseare always removed from the trace__crystal_mainbut we show it asmain, because that will have the line number in the "main" file, and we remove the actualmainfrom the trace.I also formatted the output a bit more like how Ruby shows it.
The result, without running
dsymutilon Mac, is:With
dsymutil, or in linux, the output is:I think it's a bit better than before.
As a comparison, this code in Ruby:
Gives this output:
I'll send a separate PR to always try to execute
dsymutilon Mac so we always get files and line numbers (tracked in #4186).We should also investigate why line numbers are incorrect, but probably in a different issue.