[HN Gopher] Local Variables as Accidental Breadcrumbs for Faster...
___________________________________________________________________
Local Variables as Accidental Breadcrumbs for Faster Debugging
Author : thunderbong
Score : 32 points
Date : 2024-10-06 02:50 UTC (4 days ago)
(HTM) web link (www.bugsink.com)
(TXT) w3m dump (www.bugsink.com)
| forrestthewoods wrote:
| I bloody hate Python stacktraces because they usually don't have
| enough information to fix the bug. The curse of dynamic
| languages.
|
| What's the easiest possible way to cause stacktraces to also dump
| local variable information? I feel like this is a feature that
| should be built into the language...
| aidos wrote:
| I love debugging Python. The stacktraces are great when logged
| through Sentry so even on production I can normally spot the
| bug immediately.
|
| On my local machine it's even better because I can run the
| code, let it break and then jump straight into the debugger
| where I can move up and down through the stack. I can sit in
| the context at any point and play with the data. I can call any
| Python function I want to see how the code behaves.
| vanschelven wrote:
| > The stacktraces are great when logged through Sentry
|
| <cough>Bugsink</cough> :-)
| nomel wrote:
| I don't think it's related to being a dynamic language. There
| are many "pretty exception printers" that will dump all the
| local and global variables, if you want, even up the stack!
| saila wrote:
| I don't think Python is special in this regard. I have the same
| issue with .NET/C# stack traces.
|
| With Python, you can run your program under pdb, which will
| automatically enter break on exceptions, and you can easily
| print locals.
|
| https://docs.python.org/3/library/pdb.html
| Freedom2 wrote:
| > accidental
|
| Besides a debugger, isn't one of the first things people do (even
| undergrads) is start logging out variables that may be suspect in
| the issue? If you have potentially a problematic computation, put
| it in a variable and log it out - track it and put metrics
| against it, if necessary. I'm not entirely sure a full article is
| worth it here.
| djmips wrote:
| You missed the point of the article.
| Terr_ wrote:
| What is it that you believe they missed?
|
| People have writing code in a certain way to provide logical
| "breadcrumbs" for a very long time, and doing it very
| deliberately. The fact that a tool was created that takes
| advantage of that isn't an "accident."
|
| Compare to: "Correctly-spelled words as _accidental_
| hyperlinks to the dictionary definition. "
| vanschelven wrote:
| > If you have potentially a problematic computation, put it
| in a variable and log it out
|
| My point was: what a "potentially problematic" computation is
| is not always known in advance. A style which is rich in
| local variables, when combined with a tool that shows actual
| values for all local variables when unhandled exceptions
| occur gives you this "for free". I.e. no need to log
| anything.
| jph wrote:
| > add assertions to your code.
|
| Yes, and many programming languages have assertions such as
| "assert greater than or equal to".
|
| For example with Rust and the Assertables crate:
| fn calculate_something() -> f64 { let big =
| get_big_number(); let small = get_small_number();
| assert_ge!(big, small); // >= or panic with message
| (big - small).sqrt }
|
| It turns out it's even better if your code has good error
| handling, such as a runtime assert macro that can return a result
| that is NaN (not a number) or a "maybe" result that is either Ok
| or Err.
|
| For example with Rust and the Assertables crate:
| fn calculate_something() -> Result(f64, String) { let
| big = get_big_number() let small = get_small_number()
| assert_ge!(big, small)?; // >= or return Err(message)
| (big - small).sqrt }
| aftbit wrote:
| Is there a reason you need specialized assertions over
| something like Python's generic assert? >>>
| big = 2; small = 3 >>> assert big >= small, "big is
| smaller than small" Traceback (most recent call last):
| File "<stdin>", line 1, in <module> AssertionError: big
| is smaller than small
|
| In pytest, assertions are rewritten[1] to return something more
| useful like: def test():
| big = 2 small = 3 > assert big >
| small E assert 2 > 3
| test/test_dumb.py:4: AssertionError ========== short
| test summary info ========== FAILED
| test/test_dumb.py::test - assert 2 > 3
|
| 1: https://github.com/pytest-
| dev/pytest/blob/f373974707f57a0b28...
| Arnavion wrote:
| Rust has a generic assert too, like `assert!(foo >= bar);`. I
| assume (haven't used the crate myself) the advantage of
| `assertable::assert_ge!(foo, bar)` is that it prints the
| values of foo and bar in the assert message. The
| `assert_eq!(foo, bar)` and `assert_ne!(foo, bar)` macros that
| *are* provided by Rust libstd also do this. But the generic
| `assert!()` just sees the boolean result of its expression
| and only prints that in its message.
|
| The values of the variables can be included in the generic
| macro's message via a custom message format, like
| `assert!(foo >= bar, "foo = {foo}, bar = {bar}");` but having
| the macro do it by default is convenient. There is an old
| discussion to have the `assert!()` macro parse its expression
| to figure out what variables are there and print them out by
| default, but it's still WIP. ( https://github.com/rust-
| lang/rfcs/blob/master/text/2011-gene...
| https://github.com/rust-lang/rust/issues/44838 )
| o11c wrote:
| What's more, pytest errs on the side of "just capture more"
| and In my experience it's quite useful:
| ============================= test session starts
| ============================== platform linux -- Python
| 3.11.2, pytest-7.2.1, pluggy-1.0.0+repack rootdir:
| /tmp/dl/py collected 1 item
| test_bigsmall.py F
| [100%] ===================================
| FAILURES ===================================
| ________________________________ test_something
| ________________________________ def
| test_something(): > assert get_big_number() >
| get_small_number() E assert 0 > 1 E
| + where 0 = get_big_number() E + and 1 =
| get_small_number() test_bigsmall.py:8:
| AssertionError =========================== short test
| summary info ============================ FAILED
| test_bigsmall.py::test_something - assert 0 > 1
| ============================== 1 failed in 0.06s
| ===============================
| rafaelbco wrote:
| In Zope you can create a local variable named __traceback_info__
| and its value will be inserted in the traceback. It is very
| useful.
|
| Like add a line to a log, but only when an traceback is shown.
|
| See:
| https://zopeexceptions.readthedocs.io/en/latest/narr.html#tr...
|
| Seems like the zope.exceptions package can be used independent
| from Zope.
| Terr_ wrote:
| > Accidental
|
| What? For whom? I've been _extremely intentionally_ breaking up
| longer expressions into separate lines with local variables for a
| long time.
|
| Writing local variables as "breadcrumbs" to trace what happens is
| one of the very first things new developers are taught to do,
| along with a print statement. I'd wager using a "just to break
| things up" local variable is about as common as using them to
| avoid recomputing an expression.
|
| ... Perhaps the author started out with something in the style of
| Haskell or Elm, and casual/gratuitous use of named local
| variables is new from that perspective?
|
| > However, the local variables are a different kind of
| breadcrumbs. They're not explicitly set by the developer, but
| they are there anyway.
|
| While I may not have manually designated each onto a "capture by
| a third-party addon called Bugsink" whitelist, each one is very
| explicitly "set" when I'm deciding on their names and assigning
| values to them.
| vanschelven wrote:
| > accidental
|
| Admittedly this may not be the best choice of words... but it
| was a good trade-off of length/clarity at the time for me.
|
| The longer version is: an _ideal_ programming language (from
| the perspective of debugging, though not all other
| perspectives) would just allow a full reverse playback through
| time from the point-of-failure to an arbitrary point in the
| past. A (small) step towards that is the "Breadcrumb" as
| introduced by Sentry; a hint at what happened before an error
| occurred. I argue that, in the coding-style as discussed, and
| when exposing local variables in stacktraces, local variables
| actually serve as breadcrumbs, albeit not explicitly set using
| the breadcrumb-tooling.
|
| > along with a print statement
|
| yeah but the point is that in this combination of coding style
| and tooling print statements become redundant
|
| > third-party addon called Bugsink
|
| If by third-party you mean "the data flows to a third party"
| you're mistaken, Bugsink is explicitly made to keep the data
| with you. If by "third party" you mean "not written by either
| myself or the creators of my language of choice, you're right.
| lapcat wrote:
| > Should you really change your coding style just for better
| debugging? My personal answer is: not _just_ for that, but it's
| one thing to keep in the back of your mind.
|
| My personal answer is yes, absolutely.
|
| 15 years ago I wrote a blog post "Local variables are free":
| https://lapcatsoftware.com/blog/2009/12/19/local-variables-a...
|
| Updated 7 years ago for Swift:
| https://lapcatsoftware.com/articles/local-variables-are-stil...
| isx726552 wrote:
| Just wanted to say "thank you" for this article. I found it
| years ago, probably not long after you initially wrote it and
| have preached it as widely as possible ever since, both as an
| IC and as an eng manager. It's one of the best such tidbits
| I've ever come across!
|
| Edited to add: and thanks for keeping it up to date with the
| new Swift version!
| Terr_ wrote:
| And where they aren't effectively "free", either your project
| doesn't need that performance or you're using the wrong
| language for the job. :p
|
| (I have a lot of frustration around modern software taking a
| ton of CPU power to do almost nothing, but local variables
| aren't to blame for that.)
| spullara wrote:
| "If you have nested method calls on one line of code, you can't
| easily set a breakpoint in the middle."
|
| You can now do this in Jetbrains products. Pretty awesome, you
| can even step through them.
| tetha wrote:
| This is kind of related to a change I recently made to how I
| structure variables in ansible. Part of that is because doing
| even mildly interesting transformations in ansible, filters and
| jinja is just as fun as sorting dirty needles, glass shards and
| rusty nails by hand, but what are you gonna do.
|
| But I've started to group variables into two groups: Things users
| aka my fellow admins are supposed to configure, and intermediate
| calculation steps.
|
| Things the user has to pass to use the thing should be a
| question, or they should be something the user kind of has around
| at the moment. So I now have an input variable called
| "does_dc_use_dhcp". The user can answer this concrete question,
| or recognize if the answer is wrong. Or similarly, godot and
| other frameworks offer a Vector.bounce(normal) and if you poke
| around, you find a Collision.normal and it's just matching shapes
| - normal probably goes into normal?
|
| And on the other hand, I kinda try to decompose more complex
| calculations into intermediate expressions which should be
| "obviously correct" as much as possible. Like,
| 'has_several_network_facing_interfaces: "{{
| network_facing_interfaces | length > 0 }}"'. Or something like
| 'can_use_dhcp_dns: "{{ dc_has_dhcp and
| dhcp_pushes_right_dns_servers }}'.
|
| We also had something like 'network_facing_interfaces: "{{
| ansible_interfaces | rejectattr(name='lo') }}"'. This was correct
| on a lot of systems. Until it ran into a system running docker.
| But it was easy to debug because a colleague quickly wondered why
| docker0 or any of the veth-* interfaces were flagged as network-
| facing, which they aren't?
|
| It does take work to get it to this kind of quality, but it is
| very nice to get there.
| drewg123 wrote:
| The problem I always have with locals (in kernel code written in
| C) is that the compiler tends to optimize them away, and gdb
| can't find them. So I end up having to read the assembly and try
| to figure out where values in various registers came from.
| rqtwteye wrote:
| I have done this since a long time. I always thought I am too
| dumb to read and debug complex code with multiple function calls
| in one line. I always put intermediate results into variables.
| Makes debugging so much easier.
| robdar wrote:
| I suggested this on a thread in /r/cpp a few years ago, and was
| downvoted heavily, and chewed out for the reason that coding for
| ease of debugging was apparently akin to baby killing.
| jmull wrote:
| I'm a fan of this.
|
| Not just for debugging either. Giving something a name gets you
| to think about what a good name would be, which gets you thinking
| about the nature of the thing, which clarifies your thinking
| about the thing, and leads you to better code.
|
| When I've struggled to figure out what the right name for
| something is, I sometimes realize it's hard because the thing
| doesn't really make sense. E.g., I might find I want to name two
| different things the same, which leads me to understand I was
| confused about the abstractions I was juggling.
|
| But it's also always nice to have a place to drop a break point
| or to automatically see relevant values in debuggers and other
| tools.
| animal_spirits wrote:
| The Rich package has trace back support that inspects local
| variables for every stack in the trace:
| https://rich.readthedocs.io/en/stable/traceback.html
|
| Really nice to use if you need logs in the terminal
| maleldil wrote:
| Interesting how this seems to go completely against another post
| I saw here: https://steveklabnik.com/writing/against-names/
___________________________________________________________________
(page generated 2024-10-10 23:00 UTC)