rotoscope 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ require "mkmf"
3
+
4
+ $CFLAGS << ' -std=c99 -Wall -Werror -Wno-declaration-after-statement'
5
+ $defs << "-D_POSIX_SOURCE"
6
+
7
+ uthash_path = File.expand_path('../../../lib/uthash', __FILE__)
8
+ find_header('uthash.h', uthash_path) || raise
9
+
10
+ create_makefile('rotoscope/rotoscope')
@@ -0,0 +1,391 @@
1
+ #include <errno.h>
2
+ #include <ruby.h>
3
+ #include <ruby/debug.h>
4
+ #include <ruby/intern.h>
5
+ #include <ruby/version.h>
6
+ #include <stdbool.h>
7
+ #include <stdio.h>
8
+ #include <sys/file.h>
9
+
10
+ #include "callsite.h"
11
+ #include "rotoscope.h"
12
+ #include "stack.h"
13
+ #include "strmemo.h"
14
+ #include "tracepoint.h"
15
+
16
+ VALUE cRotoscope, cTracePoint;
17
+
18
+ // recursive with singleton2str
19
+ static rs_class_desc_t class2str(VALUE klass);
20
+
21
+ static unsigned long gettid() {
22
+ return NUM2ULONG(rb_obj_id(rb_thread_current()));
23
+ }
24
+
25
+ static int write_csv_header(FILE *log, const char *header) {
26
+ return fprintf(log, "%s\n", header);
27
+ }
28
+
29
+ static const char *evflag2name(rb_event_flag_t evflag) {
30
+ switch (evflag) {
31
+ case RUBY_EVENT_CALL:
32
+ case RUBY_EVENT_C_CALL:
33
+ return "call";
34
+ case RUBY_EVENT_RETURN:
35
+ case RUBY_EVENT_C_RETURN:
36
+ return "return";
37
+ default:
38
+ return "unknown";
39
+ }
40
+ }
41
+
42
+ static bool rejected_path(VALUE path, Rotoscope *config) {
43
+ for (unsigned long i = 0; i < config->blacklist_size; i++) {
44
+ if (strstr(StringValueCStr(path), config->blacklist[i])) return true;
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ static VALUE class_of_singleton(VALUE klass) {
51
+ return rb_iv_get(klass, "__attached__");
52
+ }
53
+
54
+ static bool is_class_singleton(VALUE klass) {
55
+ VALUE obj = class_of_singleton(klass);
56
+ return (RB_TYPE_P(obj, T_MODULE) || RB_TYPE_P(obj, T_CLASS));
57
+ }
58
+
59
+ static VALUE singleton2str(VALUE klass) {
60
+ if (is_class_singleton(klass)) {
61
+ VALUE obj = class_of_singleton(klass);
62
+ VALUE cached_lookup = rb_class_path_cached(obj);
63
+ VALUE name = (NIL_P(cached_lookup)) ? rb_class_name(obj) : cached_lookup;
64
+ return name;
65
+ } else // singleton of an instance
66
+ {
67
+ return singleton2str(CLASS_OF(klass));
68
+ }
69
+ }
70
+
71
+ static rs_class_desc_t class2str(VALUE klass) {
72
+ rs_class_desc_t real_class;
73
+ real_class.method_level = INSTANCE_METHOD;
74
+
75
+ VALUE cached_lookup = rb_class_path_cached(klass);
76
+ if (RTEST(cached_lookup)) {
77
+ real_class.name = cached_lookup;
78
+ } else {
79
+ if (FL_TEST(klass, FL_SINGLETON)) {
80
+ real_class.name = singleton2str(klass);
81
+ if (is_class_singleton(klass)) {
82
+ real_class.method_level = CLASS_METHOD;
83
+ }
84
+ } else {
85
+ real_class.name = rb_class_path(klass);
86
+ }
87
+ }
88
+
89
+ return real_class;
90
+ }
91
+
92
+ static rs_callsite_t tracearg_path(rb_trace_arg_t *trace_arg) {
93
+ switch (rb_tracearg_event_flag(trace_arg)) {
94
+ case RUBY_EVENT_C_RETURN:
95
+ case RUBY_EVENT_C_CALL:
96
+ return c_callsite(trace_arg);
97
+ default:
98
+ return ruby_callsite(trace_arg);
99
+ }
100
+ }
101
+
102
+ static rs_class_desc_t tracearg_class(rb_trace_arg_t *trace_arg) {
103
+ VALUE klass;
104
+ VALUE self = rb_tracearg_self(trace_arg);
105
+
106
+ if (RB_TYPE_P(self, T_MODULE) || RB_TYPE_P(self, T_OBJECT)) {
107
+ klass = CLASS_OF(self);
108
+ } else if (RB_TYPE_P(self, T_CLASS)) {
109
+ // Does the object have an attached singleton?
110
+ // If not, name based on self instead of its singleton
111
+ klass = (FL_TEST(CLASS_OF(self), FL_SINGLETON)) ? CLASS_OF(self) : self;
112
+ } else {
113
+ klass = rb_tracearg_defined_class(trace_arg);
114
+ }
115
+
116
+ return class2str(klass);
117
+ }
118
+
119
+ static VALUE tracearg_method_name(rb_trace_arg_t *trace_arg) {
120
+ return rb_sym2str(rb_tracearg_method_id(trace_arg));
121
+ }
122
+
123
+ static rs_tracepoint_t extract_full_tracevals(rb_trace_arg_t *trace_arg,
124
+ const rs_callsite_t *callsite) {
125
+ rs_class_desc_t method_owner = tracearg_class(trace_arg);
126
+ rb_event_flag_t event_flag = rb_tracearg_event_flag(trace_arg);
127
+
128
+ VALUE method_name = tracearg_method_name(trace_arg);
129
+ VALUE filepath = callsite->filepath;
130
+
131
+ return (rs_tracepoint_t){.event = evflag2name(event_flag),
132
+ .entity = method_owner.name,
133
+ .filepath = filepath,
134
+ .method_name = method_name,
135
+ .method_level = method_owner.method_level,
136
+ .lineno = callsite->lineno};
137
+ }
138
+
139
+ static bool in_fork(Rotoscope *config) { return config->pid != getpid(); }
140
+
141
+ static bool tracecmp(rs_tracepoint_t *a, rs_tracepoint_t *b) {
142
+ return (!rb_str_cmp(a->method_name, b->method_name) &&
143
+ !rb_str_cmp(a->entity, b->entity) &&
144
+ a->method_level == b->method_level);
145
+ }
146
+
147
+ static void log_raw_trace(FILE *stream, rs_tracepoint_t trace) {
148
+ fprintf(stream, RS_CSV_FORMAT "\n", RS_CSV_VALUES(trace));
149
+ }
150
+
151
+ unsigned char output_buffer[500];
152
+ static void log_stack_frame(FILE *stream, rs_stack_t *stack,
153
+ rs_strmemo_t **call_memo, rs_tracepoint_t trace,
154
+ rb_event_flag_t event) {
155
+ if (event & EVENT_CALL) {
156
+ rs_stack_frame_t frame = rs_stack_push(stack, trace);
157
+ sprintf((char *)output_buffer, RS_FLATTENED_CSV_FORMAT "\n",
158
+ RS_FLATTENED_CSV_VALUES(frame));
159
+
160
+ if (rs_strmemo_uniq(call_memo, output_buffer)) {
161
+ fputs((char *)output_buffer, stream);
162
+ }
163
+ } else if (event & EVENT_RETURN) {
164
+ if (tracecmp(&trace, &rs_stack_peek(stack)->tp)) rs_stack_pop(stack);
165
+ }
166
+ }
167
+
168
+ static void event_hook(VALUE tpval, void *data) {
169
+ Rotoscope *config = (Rotoscope *)data;
170
+
171
+ if (config->tid != gettid()) return;
172
+ if (in_fork(config)) {
173
+ rb_tracepoint_disable(config->tracepoint);
174
+ config->state = RS_OPEN;
175
+ return;
176
+ }
177
+
178
+ rb_trace_arg_t *trace_arg = rb_tracearg_from_tracepoint(tpval);
179
+ rs_callsite_t trace_path = tracearg_path(trace_arg);
180
+
181
+ if (rejected_path(trace_path.filepath, config)) return;
182
+
183
+ rs_tracepoint_t trace = extract_full_tracevals(trace_arg, &trace_path);
184
+ if (!strcmp("Rotoscope", StringValueCStr(trace.entity))) return;
185
+
186
+ if (config->flatten_output) {
187
+ rb_event_flag_t event_flag = rb_tracearg_event_flag(trace_arg);
188
+ log_stack_frame(config->log, &config->stack, &config->call_memo, trace,
189
+ event_flag);
190
+ } else {
191
+ log_raw_trace(config->log, trace);
192
+ }
193
+ }
194
+
195
+ static void close_log_handle(Rotoscope *config) {
196
+ if (config->log) {
197
+ // Stop tracing so the event hook isn't called with a NULL log
198
+ if (config->state == RS_TRACING) {
199
+ // During process cleanup, event hooks are removed and tracepoint may have
200
+ // already have been GCed, so we need a sanity check before disabling the
201
+ // tracepoint.
202
+ if (RB_TYPE_P(config->tracepoint, T_DATA) &&
203
+ CLASS_OF(config->tracepoint) == cTracePoint) {
204
+ rb_tracepoint_disable(config->tracepoint);
205
+ }
206
+ }
207
+
208
+ if (in_fork(config)) {
209
+ close(fileno(config->log));
210
+ } else {
211
+ fclose(config->log);
212
+ }
213
+ config->log = NULL;
214
+ }
215
+ }
216
+
217
+ static void rs_gc_mark(Rotoscope *config) {
218
+ rb_gc_mark(config->log_path);
219
+ rb_gc_mark(config->tracepoint);
220
+ rs_stack_mark(&config->stack);
221
+ }
222
+
223
+ void rs_dealloc(Rotoscope *config) {
224
+ close_log_handle(config);
225
+ rs_stack_free(&config->stack);
226
+ rs_strmemo_free(config->call_memo);
227
+ xfree(config->blacklist);
228
+ xfree(config);
229
+ }
230
+
231
+ static VALUE rs_alloc(VALUE klass) {
232
+ Rotoscope *config;
233
+ VALUE self =
234
+ Data_Make_Struct(klass, Rotoscope, rs_gc_mark, rs_dealloc, config);
235
+ config->log_path = Qnil;
236
+ config->tracepoint = rb_tracepoint_new(Qnil, EVENT_CALL | EVENT_RETURN,
237
+ event_hook, (void *)config);
238
+ config->pid = getpid();
239
+ config->tid = gettid();
240
+ return self;
241
+ }
242
+
243
+ static Rotoscope *get_config(VALUE self) {
244
+ Rotoscope *config;
245
+ Data_Get_Struct(self, Rotoscope, config);
246
+ return config;
247
+ }
248
+
249
+ void copy_blacklist(Rotoscope *config, VALUE blacklist) {
250
+ Check_Type(blacklist, T_ARRAY);
251
+
252
+ size_t blacklist_malloc_size =
253
+ RARRAY_LEN(blacklist) * sizeof(*config->blacklist);
254
+
255
+ for (long i = 0; i < RARRAY_LEN(blacklist); i++) {
256
+ VALUE ruby_string = RARRAY_AREF(blacklist, i);
257
+ Check_Type(ruby_string, T_STRING);
258
+ blacklist_malloc_size += RSTRING_LEN(ruby_string) + 1;
259
+ }
260
+
261
+ config->blacklist = ruby_xmalloc(blacklist_malloc_size);
262
+ config->blacklist_size = RARRAY_LEN(blacklist);
263
+ char *str = (char *)(config->blacklist + config->blacklist_size);
264
+
265
+ for (unsigned long i = 0; i < config->blacklist_size; i++) {
266
+ VALUE ruby_string = RARRAY_AREF(blacklist, i);
267
+
268
+ config->blacklist[i] = str;
269
+ memcpy(str, RSTRING_PTR(ruby_string), RSTRING_LEN(ruby_string));
270
+ str += RSTRING_LEN(ruby_string);
271
+ *str = '\0';
272
+ str++;
273
+ }
274
+ }
275
+
276
+ VALUE initialize(int argc, VALUE *argv, VALUE self) {
277
+ Rotoscope *config = get_config(self);
278
+ VALUE output_path, blacklist, flatten;
279
+
280
+ rb_scan_args(argc, argv, "12", &output_path, &blacklist, &flatten);
281
+ Check_Type(output_path, T_STRING);
282
+
283
+ if (!NIL_P(blacklist)) {
284
+ copy_blacklist(config, blacklist);
285
+ }
286
+
287
+ config->flatten_output = RTEST(flatten);
288
+ config->log_path = output_path;
289
+ config->log = fopen(StringValueCStr(config->log_path), "w");
290
+
291
+ if (config->log == NULL) {
292
+ fprintf(stderr, "\nERROR: Failed to open file handle at %s (%s)\n",
293
+ StringValueCStr(config->log_path), strerror(errno));
294
+ exit(1);
295
+ }
296
+
297
+ if (config->flatten_output)
298
+ write_csv_header(config->log, RS_FLATTENED_CSV_HEADER);
299
+ else
300
+ write_csv_header(config->log, RS_CSV_HEADER);
301
+
302
+ rs_stack_init(&config->stack, STACK_CAPACITY);
303
+ config->call_memo = NULL;
304
+ config->state = RS_OPEN;
305
+ return self;
306
+ }
307
+
308
+ VALUE rotoscope_start_trace(VALUE self) {
309
+ Rotoscope *config = get_config(self);
310
+ rb_tracepoint_enable(config->tracepoint);
311
+ config->state = RS_TRACING;
312
+ return Qnil;
313
+ }
314
+
315
+ VALUE rotoscope_stop_trace(VALUE self) {
316
+ Rotoscope *config = get_config(self);
317
+ if (rb_tracepoint_enabled_p(config->tracepoint)) {
318
+ rb_tracepoint_disable(config->tracepoint);
319
+ config->state = RS_OPEN;
320
+ }
321
+
322
+ return Qnil;
323
+ }
324
+
325
+ VALUE rotoscope_log_path(VALUE self) {
326
+ Rotoscope *config = get_config(self);
327
+ return config->log_path;
328
+ }
329
+
330
+ VALUE rotoscope_mark(int argc, VALUE *argv, VALUE self) {
331
+ VALUE str;
332
+ rb_scan_args(argc, argv, "01", &str);
333
+
334
+ if (NIL_P(str)) str = rb_str_new2("");
335
+ Check_Type(str, T_STRING);
336
+
337
+ Rotoscope *config = get_config(self);
338
+ if (config->log != NULL && !in_fork(config)) {
339
+ rs_stack_reset(&config->stack, STACK_CAPACITY);
340
+ rs_strmemo_free(config->call_memo);
341
+ fprintf(config->log, "--- %s\n", StringValueCStr(str));
342
+ }
343
+ return Qnil;
344
+ }
345
+
346
+ VALUE rotoscope_close(VALUE self) {
347
+ Rotoscope *config = get_config(self);
348
+ if (config->state == RS_CLOSED) {
349
+ return Qtrue;
350
+ }
351
+ close_log_handle(config);
352
+ config->state = RS_CLOSED;
353
+ return Qtrue;
354
+ }
355
+
356
+ VALUE rotoscope_trace(VALUE self) {
357
+ rotoscope_start_trace(self);
358
+ return rb_ensure(rb_yield, Qundef, rotoscope_stop_trace, self);
359
+ }
360
+
361
+ VALUE rotoscope_state(VALUE self) {
362
+ Rotoscope *config = get_config(self);
363
+ switch (config->state) {
364
+ case RS_OPEN:
365
+ return ID2SYM(rb_intern("open"));
366
+ case RS_TRACING:
367
+ return ID2SYM(rb_intern("tracing"));
368
+ default:
369
+ return ID2SYM(rb_intern("closed"));
370
+ }
371
+ }
372
+
373
+ void Init_rotoscope(void) {
374
+ cTracePoint = rb_const_get(rb_cObject, rb_intern("TracePoint"));
375
+
376
+ cRotoscope = rb_define_class("Rotoscope", rb_cObject);
377
+ rb_define_alloc_func(cRotoscope, rs_alloc);
378
+ rb_define_method(cRotoscope, "initialize", initialize, -1);
379
+ rb_define_method(cRotoscope, "trace", (VALUE(*)(ANYARGS))rotoscope_trace, 0);
380
+ rb_define_method(cRotoscope, "mark", (VALUE(*)(ANYARGS))rotoscope_mark, -1);
381
+ rb_define_method(cRotoscope, "close", (VALUE(*)(ANYARGS))rotoscope_close, 0);
382
+ rb_define_method(cRotoscope, "start_trace",
383
+ (VALUE(*)(ANYARGS))rotoscope_start_trace, 0);
384
+ rb_define_method(cRotoscope, "stop_trace",
385
+ (VALUE(*)(ANYARGS))rotoscope_stop_trace, 0);
386
+ rb_define_method(cRotoscope, "log_path",
387
+ (VALUE(*)(ANYARGS))rotoscope_log_path, 0);
388
+ rb_define_method(cRotoscope, "state", (VALUE(*)(ANYARGS))rotoscope_state, 0);
389
+
390
+ init_callsite();
391
+ }
@@ -0,0 +1,65 @@
1
+ #ifndef _INC_ROTOSCOPE_H_
2
+ #define _INC_ROTOSCOPE_H_
3
+
4
+ #include <unistd.h>
5
+ #include "stack.h"
6
+ #include "strmemo.h"
7
+
8
+ #define EVENT_CALL (RUBY_EVENT_CALL | RUBY_EVENT_C_CALL)
9
+ #define EVENT_RETURN (RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN)
10
+
11
+ #define CLASS_METHOD "class"
12
+ #define INSTANCE_METHOD "instance"
13
+
14
+ #define STACK_CAPACITY 500
15
+
16
+ // clang-format off
17
+ #define _RS_COMMON_CSV_HEADER "entity,method_name,method_level,filepath,lineno"
18
+ #define _RS_COMMON_CSV_FORMAT "\"%s\",\"%s\",%s,\"%s\",%d"
19
+ #define _RS_COMMON_CSV_VALUES(trace) \
20
+ StringValueCStr(trace.entity), \
21
+ StringValueCStr(trace.method_name), \
22
+ trace.method_level, \
23
+ StringValueCStr(trace.filepath), \
24
+ trace.lineno
25
+
26
+ #define RS_CSV_HEADER "event," _RS_COMMON_CSV_HEADER
27
+ #define RS_CSV_FORMAT "%s," _RS_COMMON_CSV_FORMAT
28
+ #define RS_CSV_VALUES(trace) trace.event, _RS_COMMON_CSV_VALUES(trace)
29
+
30
+ #define RS_FLATTENED_CSV_HEADER \
31
+ _RS_COMMON_CSV_HEADER ",caller_entity,caller_method_name,caller_method_level"
32
+ #define RS_FLATTENED_CSV_FORMAT _RS_COMMON_CSV_FORMAT ",\"%s\",\"%s\",%s"
33
+ #define RS_FLATTENED_CSV_VALUES(frame) \
34
+ _RS_COMMON_CSV_VALUES(frame.tp), \
35
+ StringValueCStr(frame.caller->tp.entity), \
36
+ StringValueCStr(frame.caller->tp.method_name), \
37
+ frame.caller->tp.method_level
38
+ // clang-format on
39
+
40
+ typedef enum {
41
+ RS_CLOSED = 0,
42
+ RS_OPEN,
43
+ RS_TRACING,
44
+ } rs_state;
45
+
46
+ typedef struct {
47
+ FILE *log;
48
+ VALUE log_path;
49
+ VALUE tracepoint;
50
+ const char **blacklist;
51
+ unsigned long blacklist_size;
52
+ bool flatten_output;
53
+ pid_t pid;
54
+ unsigned long tid;
55
+ rs_state state;
56
+ rs_stack_t stack;
57
+ rs_strmemo_t *call_memo;
58
+ } Rotoscope;
59
+
60
+ typedef struct {
61
+ VALUE name;
62
+ const char *method_level;
63
+ } rs_class_desc_t;
64
+
65
+ #endif