Thread.each_caller_location was added to Ruby 3.2 as part of [Feature #16663] and is a very useful API for emitting warnings with a proper source location and similar use cases.
However in many of the cases where I used it, or seen it used, it was needed to skip the first, or a couple frames:
I think omitting the first N frames is useful.
However I see little reason to have a length or Range, one can just break/return out of the Thread.each_caller_location, and it seems pretty rare to need that.
So I think it's best to only add start = 1 for now.
In fact the Range argument is already an issue for caller_locations: it requires iterating all the stack to find how many frames there are to know which frames 0..-2 should use.
That implies it needs to count how many frames are available, which is inefficient.
So we certainly wouldn't want that behavior for each_caller_location as it would ruin a significant part of its performance advantage.
(we should maybe forbid this form for caller_locations, but that should be a separate ticket)
Ah I mistyped, caller_locations(20) is of course just setting start, caller_locations(0, 20) has no problem, I'll edit my post on Redmine to use the Range form, which is a problem.
Agreed, that's the only one I want. The reason I started the issue by asking for the same parameters is purely consistency.
Now, if there are valid reasons not to support some (like you exposed), I'm totally fine with deliberately not supporting them. But what I'm trying to say is we should start with the same feature set and then selectively remove the one that may be problematic.
So I suppose at that stage the proposal would be:
Thread.each_caller_location(1)# skip first frameThread.each_caller_location(1,20)# skip first frame, yield the next 20 framesThread.each_caller_location(1..20)# ArgumentError
Right, Range is problematic, and only Range with positive indices seems an weird restriction so I think better no Range.
Thread.each_caller_location(1, 20) I think there are basically no use cases for this, so I wouldn't add it to keep things simple.
If one wants to e.g. detect the boundary between an app and a gem they should look at locations, not hardcode some frame depth limit.
It's kind of the purpose of Thread.each_caller_location to be able to dynamically detect the limit, and then an extra Integer limit feels redundant.
Could you change your proposal to only add start = 1, unless you have/find some good use cases for Thread.each_caller_location(1, 20)?
A little addition: the dicussion about length and range was considered (not ignored), but @matz (Yukihiro Matsumoto) said he didn't see the need to make it inconsistent with Kernel#caller and Kernel#caller_locations in this case.
A little addition: the dicussion about length and range was considered (not ignored), but @matz (Yukihiro Matsumoto) said he didn't see the need to make it inconsistent with Kernel#caller and Kernel#caller_locations in this case.
Thank you for the precision.
I think it is likely other Ruby implementations raise ArgumentError if the Range begin or end is negative, because it makes no sense (IOW wrong usage) to use that with Thread.each_caller_location:
it forces to walk the entire stack to count the number of frames when the point of Thread.each_caller_location is to not walk the entire stack but just the parts being yielded to the block.
I would suggest CRuby does the same for consistency.
the point of Thread.each_caller_location is to not walk the entire stack
Is it to not walk the entire stack, or simply not to generate the Backtrace::Location objects for the entire stack? My guess is that the later is most of the overhead, and the walking, while unfortunate, isn't that bad. But I haven't measured.
@byroot (Jean Boussier) It's both, but for TruffleRuby I'm pretty sure the first matters much more.
These few allocations are no big deal on the JVM, so I would expect similar on JRuby.
Walking the stack is also obviously slower with a deeper stack (e.g. Rails), while the object allocations are pretty obvious and simply the number of times the block yielded before the user breaks/returns.
I also don't see any valid use case for Thread.each_caller_location with a Range with negative begin or end, so that's why I think it's better to prevent it altogether instead of making it a performance trap (i.e. it's O(stack depth) instead of the expected O(number of locations yielded)).
(the only way to make that fast would be to maintain a stack depth field in every frame, but that's overhead in time and memory for every call just for this feature, which seems clearly unreasonable).
I also don't see any valid use case for Thread.each_caller_location with a Range with negative begin or end
It's still a slightly cheaper version of caller_locations(range).each. I agree that it's not super useful, but I don't think it hurts being there either, and I prefer consistency.