backtracie 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +4 -0
- data/README.adoc +102 -2
- data/backtracie.gemspec +8 -2
- data/ext/backtracie_native_extension/backtracie.c +264 -78
- data/ext/backtracie_native_extension/extconf.rb +44 -0
- data/ext/backtracie_native_extension/ruby_shards.c +585 -0
- data/ext/backtracie_native_extension/ruby_shards.h +175 -0
- data/lib/backtracie/location.rb +12 -1
- data/lib/backtracie/version.rb +1 -1
- metadata +8 -16
- data/.gitignore +0 -33
- data/.rspec +0 -2
- data/.ruby-version +0 -1
- data/.standard.yml +0 -14
- data/DEVELOPMENT_NOTES.adoc +0 -399
- data/Rakefile +0 -29
- data/bin/console +0 -8
- data/bin/setup +0 -8
- data/ext/backtracie_native_extension/ruby_3.0.0.c +0 -196
- data/ext/backtracie_native_extension/ruby_3.0.0.h +0 -7
- data/gems.rb +0 -15
@@ -18,11 +18,55 @@
|
|
18
18
|
# You should have received a copy of the GNU Lesser General Public License
|
19
19
|
# along with backtracie. If not, see <http://www.gnu.org/licenses/>.
|
20
20
|
|
21
|
+
if ["jruby", "truffleruby"].include?(RUBY_ENGINE)
|
22
|
+
raise \
|
23
|
+
"\n#{"-" * 80}\nSorry! This gem is unsupported on #{RUBY_ENGINE}. Since it relies on a lot of guts of MRI Ruby, " \
|
24
|
+
"it's impossible to make a direct port.\n" \
|
25
|
+
"Perhaps a #{RUBY_ENGINE} equivalent could be created -- help is welcome! :)\n#{"-" * 80}"
|
26
|
+
end
|
27
|
+
|
21
28
|
require "mkmf"
|
22
29
|
|
23
30
|
# This warning gets really annoying when we include the Ruby mjit header file,
|
24
31
|
# let's omit it
|
25
32
|
$CFLAGS << " " << "-Wno-unused-function"
|
26
33
|
|
34
|
+
# Really dislike the "define everything at the beginning of the function" thing, sorry!
|
35
|
+
$CFLAGS << " " << "-Wno-declaration-after-statement"
|
36
|
+
|
37
|
+
# Enable us to use """modern""" C
|
38
|
+
if RUBY_VERSION < "2.4"
|
39
|
+
$CFLAGS << " " << "-std=c99"
|
40
|
+
end
|
41
|
+
|
42
|
+
# On older Rubies, we need to enable a few backports. See cfunc_frames_backport.h for details.
|
43
|
+
if RUBY_VERSION < "3"
|
44
|
+
$defs << "-DCFUNC_FRAMES_BACKPORT_NEEDED"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Backport https://github.com/ruby/ruby/pull/3084 (present in 2.7 and 3.0) to Ruby 2.6
|
48
|
+
if RUBY_VERSION.start_with?("2.6")
|
49
|
+
$defs << "-DCLASSPATH_BACKPORT_NEEDED"
|
50
|
+
end
|
51
|
+
|
52
|
+
if RUBY_VERSION < "2.6"
|
53
|
+
$defs << "-DPRE_MJIT_RUBY"
|
54
|
+
end
|
55
|
+
|
27
56
|
create_header
|
57
|
+
|
58
|
+
# The Ruby MJIT header is always (afaik?) suffixed with the exact RUBY version,
|
59
|
+
# including patch (e.g. 2.7.2). Thus, we add to the header file a definition
|
60
|
+
# containing the exact file, so that it can be used in a #include in the C code.
|
61
|
+
header_contents =
|
62
|
+
File.read($extconf_h)
|
63
|
+
.sub("#endif",
|
64
|
+
<<~EXTCONF_H.strip
|
65
|
+
#define RUBY_MJIT_HEADER "rb_mjit_min_header-#{RUBY_VERSION}.h"
|
66
|
+
|
67
|
+
#endif
|
68
|
+
EXTCONF_H
|
69
|
+
)
|
70
|
+
File.open($extconf_h, "w") { |file| file.puts(header_contents) }
|
71
|
+
|
28
72
|
create_makefile "backtracie_native_extension"
|
@@ -0,0 +1,585 @@
|
|
1
|
+
// backtracie: Ruby gem for beautiful backtraces
|
2
|
+
// Copyright (C) 2021 Ivo Anjo <ivo@ivoanjo.me>
|
3
|
+
//
|
4
|
+
// This file is part of backtracie.
|
5
|
+
//
|
6
|
+
// backtracie is free software: you can redistribute it and/or modify
|
7
|
+
// it under the terms of the GNU Lesser General Public License as published by
|
8
|
+
// the Free Software Foundation, either version 3 of the License, or
|
9
|
+
// (at your option) any later version.
|
10
|
+
//
|
11
|
+
// backtracie is distributed in the hope that it will be useful,
|
12
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
// GNU Lesser General Public License for more details.
|
15
|
+
//
|
16
|
+
// You should have received a copy of the GNU Lesser General Public License
|
17
|
+
// along with backtracie. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
// -----------------------------------------------------------------------------
|
20
|
+
// The file below has modified versions of code extracted from the Ruby project.
|
21
|
+
// The Ruby project copyright and license follow:
|
22
|
+
// -----------------------------------------------------------------------------
|
23
|
+
|
24
|
+
// Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
|
25
|
+
// You can redistribute it and/or modify it under either the terms of the
|
26
|
+
// 2-clause BSDL (see the file BSDL), or the conditions below:
|
27
|
+
|
28
|
+
// 1. You may make and give away verbatim copies of the source form of the
|
29
|
+
// software without restriction, provided that you duplicate all of the
|
30
|
+
// original copyright notices and associated disclaimers.
|
31
|
+
|
32
|
+
// 2. You may modify your copy of the software in any way, provided that
|
33
|
+
// you do at least ONE of the following:
|
34
|
+
|
35
|
+
// a. place your modifications in the Public Domain or otherwise
|
36
|
+
// make them Freely Available, such as by posting said
|
37
|
+
// modifications to Usenet or an equivalent medium, or by allowing
|
38
|
+
// the author to include your modifications in the software.
|
39
|
+
|
40
|
+
// b. use the modified software only within your corporation or
|
41
|
+
// organization.
|
42
|
+
|
43
|
+
// c. give non-standard binaries non-standard names, with
|
44
|
+
// instructions on where to get the original software distribution.
|
45
|
+
|
46
|
+
// d. make other distribution arrangements with the author.
|
47
|
+
|
48
|
+
// 3. You may distribute the software in object code or binary form,
|
49
|
+
// provided that you do at least ONE of the following:
|
50
|
+
|
51
|
+
// a. distribute the binaries and library files of the software,
|
52
|
+
// together with instructions (in the manual page or equivalent)
|
53
|
+
// on where to get the original distribution.
|
54
|
+
|
55
|
+
// b. accompany the distribution with the machine-readable source of
|
56
|
+
// the software.
|
57
|
+
|
58
|
+
// c. give non-standard binaries non-standard names, with
|
59
|
+
// instructions on where to get the original software distribution.
|
60
|
+
|
61
|
+
// d. make other distribution arrangements with the author.
|
62
|
+
|
63
|
+
// 4. You may modify and include the part of the software into any other
|
64
|
+
// software (possibly commercial). But some files in the distribution
|
65
|
+
// are not written by the author, so that they are not under these terms.
|
66
|
+
|
67
|
+
// For the list of those files and their copying conditions, see the
|
68
|
+
// file LEGAL.
|
69
|
+
|
70
|
+
// 5. The scripts and library files supplied as input to or produced as
|
71
|
+
// output from the software do not automatically fall under the
|
72
|
+
// copyright of the software, but belong to whomever generated them,
|
73
|
+
// and may be sold commercially, and may be aggregated with this
|
74
|
+
// software.
|
75
|
+
|
76
|
+
// 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
77
|
+
// IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
78
|
+
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
79
|
+
// PURPOSE.
|
80
|
+
|
81
|
+
// -----------------------------------------------------------------------------
|
82
|
+
|
83
|
+
// ruby_shards.c contains a number of borrowed functions from the MRI Ruby (usually 3.0.0) source tree:
|
84
|
+
// * A few were copy-pasted verbatim, and are dependencies for other functions
|
85
|
+
// * A few were copy-pasted and then changed so we can add features and fixes
|
86
|
+
// * A few were copy-pasted verbatim, with the objective of backporting their 3.0.0 behavior to earlier Ruby versions
|
87
|
+
// `git blame` usually documents which functions were added for what reason.
|
88
|
+
|
89
|
+
// Note that since the RUBY_MJIT_HEADER is a very special header, meant for internal use only, it has a number of quirks:
|
90
|
+
//
|
91
|
+
// 1. I've seen a few segfaults when trying to call back into original Ruby functions. E.g. even if the API is used
|
92
|
+
// correctly, just the mere inclusion of RUBY_MJIT_HEADER causes usage to crash. Thus, as much as possible, it's
|
93
|
+
// better to define functions OUTSIDE this file.
|
94
|
+
//
|
95
|
+
// 2. On Windows, I've observed "multiple definition of `something...'" (such as `rb_vm_ep_local_ep') whenever there
|
96
|
+
// are multiple files in the codebase that include the RUBY_MJIT_HEADER.
|
97
|
+
// It looks like (some?) Windows versions of Ruby define a bunch of functions in the RUBY_MJIT_HEADER itself
|
98
|
+
// without marking them as "static" (e.g. not visible to the outside of the file), and thus the linker then complains
|
99
|
+
// when linking together several files which all have these non-private symbols.
|
100
|
+
// One possible hacky solution suggested by the internets is to use the "-Wl,-allow-multiple-definition" linker
|
101
|
+
// flags to ignore this problem; instead I've chosen to implement all usage of the RUBY_MJIT_HEADER on this file --
|
102
|
+
// no other file in backtracie shall include RUBY_MJIT_HEADER.
|
103
|
+
// It's a simpler approach, and hopefully avoids any problems.
|
104
|
+
|
105
|
+
#include "extconf.h"
|
106
|
+
|
107
|
+
#ifndef PRE_MJIT_RUBY
|
108
|
+
|
109
|
+
#ifndef RUBY_MJIT_HEADER_INCLUDED
|
110
|
+
#define RUBY_MJIT_HEADER_INCLUDED
|
111
|
+
#include RUBY_MJIT_HEADER
|
112
|
+
#endif
|
113
|
+
|
114
|
+
#include "ruby_shards.h"
|
115
|
+
|
116
|
+
/**********************************************************************
|
117
|
+
vm_backtrace.c -
|
118
|
+
$Author: ko1 $
|
119
|
+
created at: Sun Jun 03 00:14:20 2012
|
120
|
+
Copyright (C) 1993-2012 Yukihiro Matsumoto
|
121
|
+
**********************************************************************/
|
122
|
+
|
123
|
+
inline static int
|
124
|
+
calc_lineno(const rb_iseq_t *iseq, const VALUE *pc)
|
125
|
+
{
|
126
|
+
VM_ASSERT(iseq);
|
127
|
+
VM_ASSERT(iseq->body);
|
128
|
+
VM_ASSERT(iseq->body->iseq_encoded);
|
129
|
+
VM_ASSERT(iseq->body->iseq_size);
|
130
|
+
if (! pc) {
|
131
|
+
/* This can happen during VM bootup. */
|
132
|
+
VM_ASSERT(iseq->body->type == ISEQ_TYPE_TOP);
|
133
|
+
VM_ASSERT(! iseq->body->local_table);
|
134
|
+
VM_ASSERT(! iseq->body->local_table_size);
|
135
|
+
return 0;
|
136
|
+
}
|
137
|
+
else {
|
138
|
+
ptrdiff_t n = pc - iseq->body->iseq_encoded;
|
139
|
+
VM_ASSERT(n <= iseq->body->iseq_size);
|
140
|
+
VM_ASSERT(n >= 0);
|
141
|
+
ASSUME(n >= 0);
|
142
|
+
size_t pos = n; /* no overflow */
|
143
|
+
if (LIKELY(pos)) {
|
144
|
+
/* use pos-1 because PC points next instruction at the beginning of instruction */
|
145
|
+
pos--;
|
146
|
+
}
|
147
|
+
return rb_iseq_line_no(iseq, pos);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
static VALUE
|
152
|
+
id2str(ID id)
|
153
|
+
{
|
154
|
+
VALUE str = rb_id2str(id);
|
155
|
+
if (!str) return Qnil;
|
156
|
+
return str;
|
157
|
+
}
|
158
|
+
#define rb_id2str(id) id2str(id)
|
159
|
+
|
160
|
+
// Hacked version of Ruby's rb_profile_frames from Ruby 3.0.0 with the following changes:
|
161
|
+
// 1. Instead of just using the rb_execution_context_t for the current thread, the context is received as an argument,
|
162
|
+
// thus allowing the sampling of any thread in the VM, not just the current one.
|
163
|
+
// 2. It gathers a lot more data: originally you'd get only a VALUE (either iseq or the cme, depending on the case),
|
164
|
+
// and the line number. The hacked version returns a whole raw_location with a lot more info.
|
165
|
+
// 3. It correctly ignores the dummy frame at the bottom of the main Ruby thread stack, thus mimicking the behavior of
|
166
|
+
// Ruby's backtrace_each (which is the function that is used to implement Thread#backtrace and friends)
|
167
|
+
// 4. Removed the start argument (upstream was broken anyway -- https://github.com/ruby/ruby/pull/2713 -- so we can
|
168
|
+
// re-add later if needed)
|
169
|
+
static int backtracie_rb_profile_frames_for_execution_context(
|
170
|
+
rb_execution_context_t *ec,
|
171
|
+
int limit,
|
172
|
+
raw_location *raw_locations
|
173
|
+
) {
|
174
|
+
int i = 0;
|
175
|
+
const rb_control_frame_t *cfp = ec->cfp;
|
176
|
+
const rb_control_frame_t *end_cfp = RUBY_VM_END_CONTROL_FRAME(ec);
|
177
|
+
const rb_callable_method_entry_t *cme = 0;
|
178
|
+
|
179
|
+
// Hack #3 above: Here we go back one frame in addition to what the original Ruby rb_profile_frames method did.
|
180
|
+
// Why? According to backtrace_each() in vm_backtrace.c there's two "dummy frames" (what MRI calls them) at the
|
181
|
+
// bottom of the stack, and we need to skip them both.
|
182
|
+
// I have no idea why the original rb_profile_frames omits this. Without this, sampling `Thread.main` always
|
183
|
+
// returned one more frame than the regular MRI APIs (which use the aforementioned backtrace_each internally).
|
184
|
+
end_cfp = RUBY_VM_NEXT_CONTROL_FRAME(end_cfp);
|
185
|
+
|
186
|
+
for (i = 0; i < limit && cfp != end_cfp;) {
|
187
|
+
// Initialize the raw_location, to avoid issues
|
188
|
+
raw_locations[i].is_ruby_frame = false;
|
189
|
+
raw_locations[i].should_use_iseq = false;
|
190
|
+
raw_locations[i].should_use_cfunc_name = false;
|
191
|
+
raw_locations[i].vm_method_type = 0;
|
192
|
+
raw_locations[i].line_number = 0;
|
193
|
+
raw_locations[i].iseq = Qnil;
|
194
|
+
raw_locations[i].callable_method_entry = Qnil;
|
195
|
+
raw_locations[i].cfunc_name = Qnil;
|
196
|
+
|
197
|
+
// The current object this is getting called on!
|
198
|
+
raw_locations[i].self = cfp->self;
|
199
|
+
|
200
|
+
cme = rb_vm_frame_method_entry(cfp);
|
201
|
+
|
202
|
+
if (VM_FRAME_RUBYFRAME_P(cfp)) {
|
203
|
+
raw_locations[i].is_ruby_frame = true;
|
204
|
+
raw_locations[i].iseq = (VALUE) cfp->iseq;
|
205
|
+
|
206
|
+
if (cme) {
|
207
|
+
raw_locations[i].callable_method_entry = (VALUE) cme;
|
208
|
+
raw_locations[i].vm_method_type = cme->def->type;
|
209
|
+
}
|
210
|
+
|
211
|
+
if (!(cme && cme->def->type == VM_METHOD_TYPE_ISEQ)) {
|
212
|
+
// This comes from the original rb_profile_frames logic, which would only return the iseq when the cme
|
213
|
+
// type is not VM_METHOD_TYPE_ISEQ
|
214
|
+
raw_locations[i].should_use_iseq = true;
|
215
|
+
}
|
216
|
+
|
217
|
+
raw_locations[i].line_number = calc_lineno(cfp->iseq, cfp->pc);
|
218
|
+
|
219
|
+
i++;
|
220
|
+
} else {
|
221
|
+
if (cme && cme->def->type == VM_METHOD_TYPE_CFUNC) {
|
222
|
+
raw_locations[i].is_ruby_frame = false;
|
223
|
+
raw_locations[i].callable_method_entry = (VALUE) cme;
|
224
|
+
raw_locations[i].vm_method_type = cme->def->type;
|
225
|
+
raw_locations[i].line_number = 0;
|
226
|
+
|
227
|
+
i++;
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
|
232
|
+
}
|
233
|
+
|
234
|
+
return i;
|
235
|
+
}
|
236
|
+
|
237
|
+
int backtracie_rb_profile_frames(int limit, raw_location *raw_locations) {
|
238
|
+
return backtracie_rb_profile_frames_for_execution_context(GET_EC(), limit, raw_locations);
|
239
|
+
}
|
240
|
+
|
241
|
+
int backtracie_rb_profile_frames_for_thread(VALUE thread, int limit, raw_location *raw_locations) {
|
242
|
+
// In here we're assuming that what we got is really a Thread or its subclass. This assumption NEEDS to be verified by
|
243
|
+
// the caller, otherwise I see a segfault in your future.
|
244
|
+
rb_thread_t *thread_pointer = (rb_thread_t*) DATA_PTR(thread);
|
245
|
+
|
246
|
+
if (thread_pointer->to_kill || thread_pointer->status == THREAD_KILLED) return Qnil;
|
247
|
+
|
248
|
+
return backtracie_rb_profile_frames_for_execution_context(thread_pointer->ec, limit, raw_locations);
|
249
|
+
}
|
250
|
+
|
251
|
+
VALUE backtracie_called_id(raw_location *the_location) {
|
252
|
+
if (the_location->callable_method_entry == Qnil) return Qnil;
|
253
|
+
|
254
|
+
return ID2SYM(
|
255
|
+
((rb_callable_method_entry_t *) the_location->callable_method_entry)
|
256
|
+
->called_id
|
257
|
+
);
|
258
|
+
}
|
259
|
+
|
260
|
+
VALUE backtracie_defined_class(raw_location *the_location) {
|
261
|
+
if (the_location->callable_method_entry == Qnil) return Qnil;
|
262
|
+
|
263
|
+
return \
|
264
|
+
((rb_callable_method_entry_t *) the_location->callable_method_entry)
|
265
|
+
->defined_class;
|
266
|
+
}
|
267
|
+
|
268
|
+
VALUE backtracie_rb_vm_top_self() {
|
269
|
+
return GET_VM()->top_self;
|
270
|
+
}
|
271
|
+
|
272
|
+
bool backtracie_iseq_is_block(raw_location *the_location) {
|
273
|
+
if (the_location->iseq == Qnil) return false;
|
274
|
+
|
275
|
+
return ((rb_iseq_t *) the_location->iseq)->body->type == ISEQ_TYPE_BLOCK;
|
276
|
+
}
|
277
|
+
|
278
|
+
bool backtracie_iseq_is_eval(raw_location *the_location) {
|
279
|
+
if (the_location->iseq == Qnil) return false;
|
280
|
+
|
281
|
+
return ((rb_iseq_t *) the_location->iseq)->body->type == ISEQ_TYPE_EVAL;
|
282
|
+
}
|
283
|
+
|
284
|
+
VALUE backtracie_refinement_name(raw_location *the_location) {
|
285
|
+
VALUE defined_class = backtracie_defined_class(the_location);
|
286
|
+
if (defined_class == Qnil) return Qnil;
|
287
|
+
|
288
|
+
VALUE refinement_module = rb_class_of(defined_class);
|
289
|
+
if (!FL_TEST(refinement_module, RMODULE_IS_REFINEMENT)) return Qnil;
|
290
|
+
|
291
|
+
// The below bits are inspired by Ruby's rb_mod_to_s(VALUE)
|
292
|
+
ID id_refined_class;
|
293
|
+
CONST_ID(id_refined_class, "__refined_class__");
|
294
|
+
VALUE refined_class = rb_attr_get(refinement_module, id_refined_class);
|
295
|
+
if (refined_class == Qnil) return Qnil;
|
296
|
+
|
297
|
+
VALUE result = rb_inspect(refined_class);
|
298
|
+
rb_str_concat(result, rb_str_new2("$refinement@"));
|
299
|
+
ID id_defined_at;
|
300
|
+
CONST_ID(id_defined_at, "__defined_at__");
|
301
|
+
rb_str_concat(result, rb_inspect(rb_attr_get(refinement_module, id_defined_at)));
|
302
|
+
|
303
|
+
return result;
|
304
|
+
}
|
305
|
+
|
306
|
+
// For more details on the objective of this backport, see the comments on ruby_shards.h
|
307
|
+
// This is used for Ruby < 3.0.0
|
308
|
+
#ifdef CFUNC_FRAMES_BACKPORT_NEEDED
|
309
|
+
|
310
|
+
static const rb_callable_method_entry_t *
|
311
|
+
cframe(VALUE frame)
|
312
|
+
{
|
313
|
+
if (frame == Qnil) return NULL;
|
314
|
+
|
315
|
+
if (RB_TYPE_P(frame, T_IMEMO)) {
|
316
|
+
switch (imemo_type(frame)) {
|
317
|
+
case imemo_ment:
|
318
|
+
{
|
319
|
+
const rb_callable_method_entry_t *cme = (rb_callable_method_entry_t *)frame;
|
320
|
+
switch (cme->def->type) {
|
321
|
+
case VM_METHOD_TYPE_CFUNC:
|
322
|
+
return cme;
|
323
|
+
default:
|
324
|
+
return NULL;
|
325
|
+
}
|
326
|
+
}
|
327
|
+
default:
|
328
|
+
return NULL;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
return NULL;
|
333
|
+
}
|
334
|
+
|
335
|
+
static const rb_iseq_t *
|
336
|
+
frame2iseq(VALUE frame)
|
337
|
+
{
|
338
|
+
if (frame == Qnil) return NULL;
|
339
|
+
|
340
|
+
if (RB_TYPE_P(frame, T_IMEMO)) {
|
341
|
+
switch (imemo_type(frame)) {
|
342
|
+
case imemo_iseq:
|
343
|
+
return (const rb_iseq_t *)frame;
|
344
|
+
case imemo_ment:
|
345
|
+
{
|
346
|
+
const rb_callable_method_entry_t *cme = (rb_callable_method_entry_t *)frame;
|
347
|
+
switch (cme->def->type) {
|
348
|
+
case VM_METHOD_TYPE_ISEQ:
|
349
|
+
return cme->def->body.iseq.iseqptr;
|
350
|
+
default:
|
351
|
+
return NULL;
|
352
|
+
}
|
353
|
+
}
|
354
|
+
default:
|
355
|
+
break;
|
356
|
+
}
|
357
|
+
}
|
358
|
+
rb_bug("frame2iseq: unreachable");
|
359
|
+
}
|
360
|
+
|
361
|
+
VALUE
|
362
|
+
backported_rb_profile_frame_method_name(VALUE frame)
|
363
|
+
{
|
364
|
+
const rb_callable_method_entry_t *cme = cframe(frame);
|
365
|
+
if (cme) {
|
366
|
+
ID mid = cme->def->original_id;
|
367
|
+
return id2str(mid);
|
368
|
+
}
|
369
|
+
const rb_iseq_t *iseq = frame2iseq(frame);
|
370
|
+
return iseq ? rb_iseq_method_name(iseq) : Qnil;
|
371
|
+
}
|
372
|
+
|
373
|
+
#endif // CFUNC_FRAMES_BACKPORT_NEEDED
|
374
|
+
|
375
|
+
#ifdef CLASSPATH_BACKPORT_NEEDED
|
376
|
+
static VALUE
|
377
|
+
frame2klass(VALUE frame)
|
378
|
+
{
|
379
|
+
if (frame == Qnil) return Qnil;
|
380
|
+
|
381
|
+
if (RB_TYPE_P(frame, T_IMEMO)) {
|
382
|
+
const rb_callable_method_entry_t *cme = (rb_callable_method_entry_t *)frame;
|
383
|
+
|
384
|
+
if (imemo_type(frame) == imemo_ment) {
|
385
|
+
return cme->defined_class;
|
386
|
+
}
|
387
|
+
}
|
388
|
+
return Qnil;
|
389
|
+
}
|
390
|
+
|
391
|
+
VALUE
|
392
|
+
backported_rb_profile_frame_classpath(VALUE frame)
|
393
|
+
{
|
394
|
+
VALUE klass = frame2klass(frame);
|
395
|
+
|
396
|
+
if (klass && !NIL_P(klass)) {
|
397
|
+
if (RB_TYPE_P(klass, T_ICLASS)) {
|
398
|
+
klass = RBASIC(klass)->klass;
|
399
|
+
}
|
400
|
+
else if (FL_TEST(klass, FL_SINGLETON)) {
|
401
|
+
klass = rb_ivar_get(klass, id__attached__);
|
402
|
+
if (!RB_TYPE_P(klass, T_CLASS) && !RB_TYPE_P(klass, T_MODULE))
|
403
|
+
return rb_sprintf("#<%s:%p>", rb_class2name(rb_obj_class(klass)), (void*)klass);
|
404
|
+
}
|
405
|
+
return rb_class_path(klass);
|
406
|
+
}
|
407
|
+
else {
|
408
|
+
return Qnil;
|
409
|
+
}
|
410
|
+
}
|
411
|
+
|
412
|
+
// Oddly enough, this method is on debug.h BUT NOT on the MJIT header. Since I've had
|
413
|
+
// crashes when trying to combine the MJIT header with the regular Ruby headers, let's
|
414
|
+
// just supply the declaration for this function, as otherwise the build seems to fail
|
415
|
+
// on macOS in CI.
|
416
|
+
VALUE rb_profile_frame_singleton_method_p(VALUE frame);
|
417
|
+
|
418
|
+
static VALUE
|
419
|
+
qualified_method_name(VALUE frame, VALUE method_name)
|
420
|
+
{
|
421
|
+
if (method_name != Qnil) {
|
422
|
+
VALUE classpath = backported_rb_profile_frame_classpath(frame);
|
423
|
+
VALUE singleton_p = rb_profile_frame_singleton_method_p(frame);
|
424
|
+
|
425
|
+
if (classpath != Qnil) {
|
426
|
+
return rb_sprintf("%"PRIsVALUE"%s%"PRIsVALUE,
|
427
|
+
classpath, singleton_p == Qtrue ? "." : "#", method_name);
|
428
|
+
}
|
429
|
+
else {
|
430
|
+
return method_name;
|
431
|
+
}
|
432
|
+
}
|
433
|
+
else {
|
434
|
+
return Qnil;
|
435
|
+
}
|
436
|
+
}
|
437
|
+
|
438
|
+
VALUE
|
439
|
+
backported_rb_profile_frame_qualified_method_name(VALUE frame)
|
440
|
+
{
|
441
|
+
VALUE method_name = backported_rb_profile_frame_method_name(frame);
|
442
|
+
|
443
|
+
return qualified_method_name(frame, method_name);
|
444
|
+
}
|
445
|
+
#endif // CLASSPATH_BACKPORT_NEEDED
|
446
|
+
|
447
|
+
#endif // ifndef PRE_MJIT_RUBY
|
448
|
+
|
449
|
+
// -----------------------------------------------------------------------------
|
450
|
+
|
451
|
+
#ifdef PRE_MJIT_RUBY
|
452
|
+
|
453
|
+
#include <stdbool.h>
|
454
|
+
|
455
|
+
#include "ruby.h"
|
456
|
+
|
457
|
+
#include "ruby_shards.h"
|
458
|
+
|
459
|
+
typedef struct rb_backtrace_location_struct {
|
460
|
+
enum LOCATION_TYPE {
|
461
|
+
LOCATION_TYPE_ISEQ = 1,
|
462
|
+
LOCATION_TYPE_ISEQ_CALCED,
|
463
|
+
LOCATION_TYPE_CFUNC,
|
464
|
+
} type;
|
465
|
+
|
466
|
+
union {
|
467
|
+
struct {
|
468
|
+
const VALUE iseq; // Originally const rb_iseq_t *
|
469
|
+
union {
|
470
|
+
const VALUE *pc;
|
471
|
+
int lineno;
|
472
|
+
} lineno;
|
473
|
+
} iseq;
|
474
|
+
struct {
|
475
|
+
ID mid;
|
476
|
+
struct rb_backtrace_location_struct *prev_loc;
|
477
|
+
} cfunc;
|
478
|
+
} body;
|
479
|
+
} rb_backtrace_location_t;
|
480
|
+
|
481
|
+
struct valued_frame_info {
|
482
|
+
rb_backtrace_location_t *loc;
|
483
|
+
VALUE btobj;
|
484
|
+
};
|
485
|
+
|
486
|
+
// For Ruby < 2.6, we can't rely on the MJIT header. So we need to get... crafty. This alternative to
|
487
|
+
// backtracie_rb_profile_frames_for_execution_context(...) piggybacks on the actual
|
488
|
+
// Thread::Backtrace::Locations instances returned by Ruby's Thread#backtrace_locations, and then uses
|
489
|
+
// knowledge of the VM internal layouts to get the data we need out of them.
|
490
|
+
//
|
491
|
+
// Currently, this approach gets us a lot less information than the Ruby >= 2.6 approach (e.g. currently we
|
492
|
+
// get exactly the same as Thread::Backtrace::Locations offers), so it's a crappier replacement, but it does
|
493
|
+
// get us support for older Rubies so it's better than nothing (to check the differences in behavior, check which
|
494
|
+
// tests are disabled on < 2.6).
|
495
|
+
int backtracie_profile_frames_from_ruby_locations(
|
496
|
+
VALUE ruby_locations_array,
|
497
|
+
raw_location *raw_locations
|
498
|
+
) {
|
499
|
+
Check_Type(ruby_locations_array, T_ARRAY);
|
500
|
+
int ruby_locations_array_size = RARRAY_LEN(ruby_locations_array);
|
501
|
+
|
502
|
+
for (int i = 0; i < ruby_locations_array_size; i++) {
|
503
|
+
// FIXME: We should validate that what we get out of the array is a Thread::Backtrace::Location instance
|
504
|
+
VALUE location = rb_ary_entry(ruby_locations_array, i);
|
505
|
+
|
506
|
+
// The leap of faith -- let's get into the data from the Location instance
|
507
|
+
struct valued_frame_info *location_payload = DATA_PTR(location);
|
508
|
+
|
509
|
+
if (location_payload->loc->type == LOCATION_TYPE_ISEQ) {
|
510
|
+
// Trigger calculation of the line number; this is calculated lazily when this method is invoked and
|
511
|
+
// it turns the location type from LOCATION_TYPE_ISEQ to LOCATION_TYPE_ISEQ_CALCED.
|
512
|
+
rb_funcall(location, rb_intern("lineno"), 0);
|
513
|
+
|
514
|
+
if (location_payload->loc->type == LOCATION_TYPE_ISEQ) {
|
515
|
+
rb_raise(rb_eRuntimeError, "Internal error: Calling #lineno didn't turn a location into a LOCATION_TYPE_ISEQ_CALCED");
|
516
|
+
}
|
517
|
+
}
|
518
|
+
|
519
|
+
if (location_payload->loc->type == LOCATION_TYPE_ISEQ || location_payload->loc->type == LOCATION_TYPE_ISEQ_CALCED) {
|
520
|
+
// Ruby frame
|
521
|
+
raw_locations[i].is_ruby_frame = true;
|
522
|
+
raw_locations[i].should_use_iseq = true;
|
523
|
+
raw_locations[i].should_use_cfunc_name = false;
|
524
|
+
raw_locations[i].vm_method_type = -1;
|
525
|
+
// FIXME: Poke MRI to always get the "calced" version, see location_lineno in vm_backtrace for the context
|
526
|
+
raw_locations[i].line_number =
|
527
|
+
location_payload->loc->type == LOCATION_TYPE_ISEQ_CALCED ?
|
528
|
+
location_payload->loc->body.iseq.lineno.lineno : 0;
|
529
|
+
raw_locations[i].iseq = (VALUE) location_payload->loc->body.iseq.iseq;
|
530
|
+
raw_locations[i].callable_method_entry = Qnil;
|
531
|
+
raw_locations[i].self = Qnil;
|
532
|
+
raw_locations[i].cfunc_name = Qnil;
|
533
|
+
} else {
|
534
|
+
// cfunc frame
|
535
|
+
raw_locations[i].is_ruby_frame = false;
|
536
|
+
raw_locations[i].should_use_iseq = false;
|
537
|
+
raw_locations[i].should_use_cfunc_name = true;
|
538
|
+
raw_locations[i].vm_method_type = -1;
|
539
|
+
raw_locations[i].line_number = 0;
|
540
|
+
raw_locations[i].iseq = Qnil;
|
541
|
+
raw_locations[i].callable_method_entry = Qnil;
|
542
|
+
raw_locations[i].self = Qnil;
|
543
|
+
raw_locations[i].cfunc_name = rb_id2str(location_payload->loc->body.cfunc.mid);
|
544
|
+
}
|
545
|
+
}
|
546
|
+
|
547
|
+
return ruby_locations_array_size;
|
548
|
+
}
|
549
|
+
|
550
|
+
VALUE backtracie_called_id(raw_location *the_location) {
|
551
|
+
// Not available on PRE_MJIT_RUBY
|
552
|
+
return Qnil;
|
553
|
+
}
|
554
|
+
|
555
|
+
VALUE backtracie_defined_class(raw_location *the_location) {
|
556
|
+
// Not available on PRE_MJIT_RUBY
|
557
|
+
return Qnil;
|
558
|
+
}
|
559
|
+
|
560
|
+
VALUE backtracie_rb_vm_top_self() {
|
561
|
+
// This is only used on gem initialization so... Let's be a bit lazy :)
|
562
|
+
return rb_eval_string("TOPLEVEL_BINDING.eval('self')");
|
563
|
+
}
|
564
|
+
|
565
|
+
bool backtracie_iseq_is_block(raw_location *the_location) {
|
566
|
+
// Not available on PRE_MJIT_RUBY
|
567
|
+
return false;
|
568
|
+
}
|
569
|
+
|
570
|
+
bool backtracie_iseq_is_eval(raw_location *the_location) {
|
571
|
+
// Not available on PRE_MJIT_RUBY
|
572
|
+
return false;
|
573
|
+
}
|
574
|
+
|
575
|
+
VALUE backported_rb_profile_frame_method_name(VALUE frame) {
|
576
|
+
// Not available on PRE_MJIT_RUBY
|
577
|
+
return Qnil;
|
578
|
+
}
|
579
|
+
|
580
|
+
VALUE backtracie_refinement_name(raw_location *the_location) {
|
581
|
+
// Not available on PRE_MJIT_RUBY
|
582
|
+
return Qnil;
|
583
|
+
}
|
584
|
+
|
585
|
+
#endif // ifdef PRE_MJIT_RUBY
|