crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.5k stars 1.62k forks source link

Getting just the top of the stack trace #13582

Open HertzDevil opened 1 year ago

HertzDevil commented 1 year ago

Sometimes, only the top few entries of the stack trace are needed, for example in https://forum.crystal-lang.org/t/memory-profiling/4888 where we do not print the entries at the bottom. When the stack is very deep, most of the time in caller.first(n) would be spent on retrieving the entries at the bottom, but surely we could simply stop unwinding the call stack after the first n entries. So IMO #caller should take an optional limit argument:

def caller(limit : Int? = nil)
end

Note that caller(n) may be shorter then caller.first(n) if some stack entries are omitted due to Exception::CallStack.skip. To actually stop the unwinding:

# libunwind.cr
struct Exception::CallStack
  protected def self.unwind(limit) : Array(Void*)
    callstack = [] of Void*
    backtrace_fn = ->(context : LibUnwind::Context, data : Void*) do
      # EDIT: this actually doesn't work because it would form a closure
      if limit
        return LibUnwind::ReasonCode::END_OF_STACK if limit.zero?
        limit -= 1
      end
      # ...
    end
  end
end
# stackwalk.cr
struct Exception::CallStack
  protected def self.unwind(limit) : Array(Void*)
    # ...
    each_frame(context, limit) do |frame|
      # ...
    end
    # ...
  end

  private def self.each_frame(context, limit, &)
    # ...
    until limit.try(&.zero?)
      limit -= 1 if limit
      # ...
    end
    # ...
  end
end

The interpreter_call_stack_unwind primitive will also need to understand this limit parameter.

straight-shoota commented 1 year ago

I'm wondering if there's any benefit from having limit nilable vs. using a high value (Int32::MAX?) as default.

HertzDevil commented 1 year ago

Right inside the body of #caller, sure, but no stdlib API does that at the method signature

straight-shoota commented 1 year ago

Well there could just be two separate signatures with no explicit default value: caller() and caller(Int).

HertzDevil commented 1 year ago

I just tried this and for non-release builds the top of the stack will invariably contain the same stack frames leading all the way to the unwinding method itself, so #caller(Int) may not be as intuitive as it seems. (Recall that unwinding the stack doesn't need debug information, but detecting Exception::CallStack.skipped frames does.)