stack_tracy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +12 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +17 -0
- data/MIT-LICENSE +20 -0
- data/README.md +367 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/benchmarks/benchmark.rb +64 -0
- data/bin/tracy +10 -0
- data/ext/stack_tracy/extconf.rb +4 -0
- data/ext/stack_tracy/stack_tracy.c +216 -0
- data/ext/stack_tracy/stack_tracy.h +48 -0
- data/lib/stack_tracy.rb +162 -0
- data/lib/stack_tracy/core_ext.rb +3 -0
- data/lib/stack_tracy/core_ext/kernel.rb +24 -0
- data/lib/stack_tracy/event_info.rb +72 -0
- data/lib/stack_tracy/sinatra.rb +36 -0
- data/lib/stack_tracy/version.rb +7 -0
- data/script/console +13 -0
- data/stack_tracy.gemspec +20 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/test_kernel.rb +13 -0
- data/test/unit/test_tracy.rb +261 -0
- data/ui/assets/bootstrap-tab.css +340 -0
- data/ui/assets/bootstrap-tab.js +135 -0
- data/ui/assets/jquery.js +2 -0
- data/ui/assets/stack_tracy.css +99 -0
- data/ui/assets/stack_tracy.js +38 -0
- data/ui/assets/tiny_sort.js +305 -0
- data/ui/index.html.erb +123 -0
- metadata +114 -0
data/bin/tracy
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
#include "stack_tracy.h"
|
2
|
+
|
3
|
+
static double nsec() {
|
4
|
+
#if defined(__linux__)
|
5
|
+
struct timespec clock;
|
6
|
+
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &clock);
|
7
|
+
return (clock.tv_sec * 1000000000 + clock.tv_nsec) / 1000000000.0;
|
8
|
+
#elif defined(_win32)
|
9
|
+
FILETIME createTime;
|
10
|
+
FILETIME exitTime;
|
11
|
+
FILETIME sysTime;
|
12
|
+
FILETIME cpuTime;
|
13
|
+
|
14
|
+
ULARGE_INTEGER sysTimeInt;
|
15
|
+
ULARGE_INTEGER cpuTimeInt;
|
16
|
+
ULONGLONG totalTime;
|
17
|
+
|
18
|
+
GetProcessTimes(GetCurrentProcess(), &createTime, &exitTime, &sysTime, &cpuTime);
|
19
|
+
|
20
|
+
/* Doing this based on MSFT's recommendation in the FILETIME structure documentation at
|
21
|
+
http://msdn.microsoft.com/en-us/library/ms724284%28VS.85%29.aspx*/
|
22
|
+
|
23
|
+
sysTimeInt.LowPart = sysTime.dwLowDateTime;
|
24
|
+
sysTimeInt.HighPart = sysTime.dwHighDateTime;
|
25
|
+
cpuTimeInt.LowPart = cpuTime.dwLowDateTime;
|
26
|
+
cpuTimeInt.HighPart = cpuTime.dwHighDateTime;
|
27
|
+
|
28
|
+
totalTime = sysTimeInt.QuadPart + cpuTimeInt.QuadPart;
|
29
|
+
|
30
|
+
// Times are in 100-nanosecond time units. So instead of 10-9 use 10-7
|
31
|
+
return totalTime / 10000000.0;
|
32
|
+
#else
|
33
|
+
return ((double) clock()) / CLOCKS_PER_SEC;
|
34
|
+
#endif
|
35
|
+
}
|
36
|
+
|
37
|
+
static const char *event_name(rb_event_flag_t event) {
|
38
|
+
switch (event) {
|
39
|
+
case RUBY_EVENT_LINE:
|
40
|
+
return "line";
|
41
|
+
case RUBY_EVENT_CLASS:
|
42
|
+
return "class";
|
43
|
+
case RUBY_EVENT_END:
|
44
|
+
return "end";
|
45
|
+
case RUBY_EVENT_CALL:
|
46
|
+
return "call";
|
47
|
+
case RUBY_EVENT_RETURN:
|
48
|
+
return "return";
|
49
|
+
case RUBY_EVENT_C_CALL:
|
50
|
+
return "c-call";
|
51
|
+
case RUBY_EVENT_C_RETURN:
|
52
|
+
return "c-return";
|
53
|
+
case RUBY_EVENT_RAISE:
|
54
|
+
return "raise";
|
55
|
+
#ifdef RUBY_VM
|
56
|
+
case RUBY_EVENT_SWITCH:
|
57
|
+
return "thread-interrupt";
|
58
|
+
#endif
|
59
|
+
default:
|
60
|
+
return "unknown";
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
#if defined(RB_EVENT_HOOKS_HAVE_CALLBACK_DATA) || defined(RUBY_EVENT_VM)
|
65
|
+
static void stack_tracy_trap(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass)
|
66
|
+
#else
|
67
|
+
static void stack_tracy_trap(rb_event_flag_t event, NODE *node, VALUE self, ID id, VALUE klass)
|
68
|
+
#endif
|
69
|
+
{
|
70
|
+
int i;
|
71
|
+
bool singleton = false, match = false;
|
72
|
+
EventInfo info;
|
73
|
+
|
74
|
+
if (event == RUBY_EVENT_CALL || event == RUBY_EVENT_C_CALL) {
|
75
|
+
trace = true;
|
76
|
+
}
|
77
|
+
|
78
|
+
if (trace == false) {
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
|
82
|
+
#ifdef RUBY_VM
|
83
|
+
if (id == 0) {
|
84
|
+
rb_frame_method_id_and_class(&id, &klass);
|
85
|
+
}
|
86
|
+
#endif
|
87
|
+
|
88
|
+
if (klass) {
|
89
|
+
if (TYPE(klass) == T_ICLASS) {
|
90
|
+
klass = RBASIC(klass)->klass;
|
91
|
+
}
|
92
|
+
|
93
|
+
singleton = FL_TEST(klass, FL_SINGLETON);
|
94
|
+
|
95
|
+
#ifdef RUBY_VM
|
96
|
+
if (singleton && !(TYPE(self) == T_CLASS || TYPE(self) == T_MODULE))
|
97
|
+
singleton = false;
|
98
|
+
#endif
|
99
|
+
}
|
100
|
+
|
101
|
+
info.method = (ID *) id;
|
102
|
+
|
103
|
+
if (info.method != NULL) {
|
104
|
+
info.object = (VALUE *)(singleton ? self : klass);
|
105
|
+
|
106
|
+
if (info.object) {
|
107
|
+
for (i = 0; i < exclude_size; i++) {
|
108
|
+
if (((VALUE) exclude[i].klass) == (VALUE) info.object) {
|
109
|
+
return;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
if (only_size > 0) {
|
114
|
+
match = false;
|
115
|
+
for (i = 0; i < only_size; i++) {
|
116
|
+
match = match || (((VALUE) only[i].klass) == (VALUE) info.object);
|
117
|
+
}
|
118
|
+
if (!match) {
|
119
|
+
return;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
info.event = event;
|
124
|
+
info.file = (char *) rb_sourcefile();
|
125
|
+
info.line = rb_sourceline();
|
126
|
+
info.singleton = singleton;
|
127
|
+
info.nsec = nsec();
|
128
|
+
|
129
|
+
stack_size = stack_size + 1;
|
130
|
+
stack = (EventInfo *) realloc (stack, stack_size * sizeof(EventInfo));
|
131
|
+
stack[stack_size - 1] = info;
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
VALUE stack_tracy_start(VALUE self, VALUE only_names, VALUE exclude_names) {
|
137
|
+
int i;
|
138
|
+
char *token;
|
139
|
+
|
140
|
+
token = strtok((char *) RSTRING_PTR(only_names), " ");
|
141
|
+
only_size = 0;
|
142
|
+
|
143
|
+
while (token != NULL) {
|
144
|
+
only_size++;
|
145
|
+
only = (RubyClass *) realloc (only, only_size * sizeof(RubyClass));
|
146
|
+
|
147
|
+
RubyClass klass;
|
148
|
+
klass.name = (char *) token;
|
149
|
+
klass.klass = (VALUE *) rb_path2class((char *) token);
|
150
|
+
only[only_size - 1] = klass;
|
151
|
+
|
152
|
+
token = strtok(NULL, " ");
|
153
|
+
}
|
154
|
+
|
155
|
+
token = strtok((char *) RSTRING_PTR(exclude_names), " ");
|
156
|
+
exclude_size = 0;
|
157
|
+
|
158
|
+
while (token != NULL) {
|
159
|
+
exclude_size++;
|
160
|
+
exclude = (RubyClass *) realloc (exclude, exclude_size * sizeof(RubyClass));
|
161
|
+
|
162
|
+
RubyClass klass;
|
163
|
+
klass.name = (char *) token;
|
164
|
+
klass.klass = (VALUE *) rb_path2class((char *) token);
|
165
|
+
exclude[exclude_size - 1] = klass;
|
166
|
+
|
167
|
+
token = strtok(NULL, " ");
|
168
|
+
}
|
169
|
+
|
170
|
+
#if defined(RB_EVENT_HOOKS_HAVE_CALLBACK_DATA) || defined(RUBY_EVENT_VM)
|
171
|
+
rb_add_event_hook(stack_tracy_trap, RUBY_EVENT_CALL | RUBY_EVENT_C_CALL | RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN, 0);
|
172
|
+
#else
|
173
|
+
rb_add_event_hook(stack_tracy_trap, RUBY_EVENT_CALL | RUBY_EVENT_C_CALL | RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN);
|
174
|
+
#endif
|
175
|
+
|
176
|
+
stack_size = 0, trace = false;
|
177
|
+
|
178
|
+
return Qnil;
|
179
|
+
}
|
180
|
+
|
181
|
+
VALUE stack_tracy_stop(VALUE self) {
|
182
|
+
VALUE events, event;
|
183
|
+
ID id;
|
184
|
+
const char *method;
|
185
|
+
int i;
|
186
|
+
|
187
|
+
rb_remove_event_hook(stack_tracy_trap);
|
188
|
+
|
189
|
+
events = rb_ary_new();
|
190
|
+
|
191
|
+
for (i = 0; i < stack_size - 2; i++) {
|
192
|
+
method = rb_id2name((ID) stack[i].method);
|
193
|
+
if (method != NULL) {
|
194
|
+
event = rb_funcall(cEventInfo, rb_intern("new"), 0);
|
195
|
+
rb_iv_set(event, "@event", rb_str_new2(event_name(stack[i].event)));
|
196
|
+
rb_iv_set(event, "@file", rb_str_new2(stack[i].file));
|
197
|
+
rb_iv_set(event, "@line", rb_int_new(stack[i].line));
|
198
|
+
rb_iv_set(event, "@singleton", stack[i].singleton);
|
199
|
+
rb_iv_set(event, "@object", (VALUE) stack[i].object);
|
200
|
+
rb_iv_set(event, "@method", rb_str_new2(method));
|
201
|
+
rb_iv_set(event, "@nsec", rb_float_new(stack[i].nsec));
|
202
|
+
rb_ary_push(events, event);
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
rb_iv_set(mStackTracy, "@stack_trace", events);
|
207
|
+
|
208
|
+
return Qnil;
|
209
|
+
}
|
210
|
+
|
211
|
+
void Init_stack_tracy() {
|
212
|
+
mStackTracy = rb_const_get(rb_cObject, rb_intern("StackTracy"));
|
213
|
+
cEventInfo = rb_const_get(mStackTracy, rb_intern("EventInfo"));
|
214
|
+
rb_define_singleton_method(mStackTracy, "_start", stack_tracy_start, 2);
|
215
|
+
rb_define_singleton_method(mStackTracy, "_stop", stack_tracy_stop, 0);
|
216
|
+
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#ifndef STACK_TRACY_H
|
2
|
+
#define STACK_TRACY_H
|
3
|
+
|
4
|
+
#include <ruby.h>
|
5
|
+
#include <stdbool.h>
|
6
|
+
#include <time.h>
|
7
|
+
#include <sys/time.h>
|
8
|
+
#include <string.h>
|
9
|
+
|
10
|
+
typedef struct event_info_t {
|
11
|
+
rb_event_flag_t event;
|
12
|
+
const char *file;
|
13
|
+
int line;
|
14
|
+
bool singleton;
|
15
|
+
const VALUE *object;
|
16
|
+
const ID *method;
|
17
|
+
double nsec;
|
18
|
+
} EventInfo;
|
19
|
+
|
20
|
+
typedef struct ruby_class_t {
|
21
|
+
const char *name;
|
22
|
+
const VALUE *klass;
|
23
|
+
} RubyClass;
|
24
|
+
|
25
|
+
static VALUE mStackTracy;
|
26
|
+
static VALUE cEventInfo;
|
27
|
+
|
28
|
+
static EventInfo *stack;
|
29
|
+
static RubyClass *only;
|
30
|
+
static RubyClass *exclude;
|
31
|
+
|
32
|
+
static int stack_size, only_size, exclude_size;
|
33
|
+
static bool trace;
|
34
|
+
|
35
|
+
static double nsec();
|
36
|
+
static const char *event_name(rb_event_flag_t event);
|
37
|
+
|
38
|
+
#if defined(RB_EVENT_HOOKS_HAVE_CALLBACK_DATA) || defined(RUBY_EVENT_VM)
|
39
|
+
static void stack_tracy_trap(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass);
|
40
|
+
#else
|
41
|
+
static void stack_tracy_trap(rb_event_flag_t event, NODE *node, VALUE self, ID id, VALUE klass);
|
42
|
+
#endif
|
43
|
+
|
44
|
+
VALUE stack_tracy_start(VALUE self, VALUE only_names, VALUE exclude_names);
|
45
|
+
VALUE stack_tracy_stop(VALUE self);
|
46
|
+
void Init_stack_tracy();
|
47
|
+
|
48
|
+
#endif
|
data/lib/stack_tracy.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "csv"
|
3
|
+
require "tmpdir"
|
4
|
+
require "securerandom"
|
5
|
+
require "rich/support/core/string/colorize"
|
6
|
+
require "launchy"
|
7
|
+
|
8
|
+
require "stack_tracy/core_ext"
|
9
|
+
require "stack_tracy/event_info"
|
10
|
+
require "stack_tracy/sinatra"
|
11
|
+
require "stack_tracy/version"
|
12
|
+
|
13
|
+
module StackTracy
|
14
|
+
extend self
|
15
|
+
|
16
|
+
PRESETS = {
|
17
|
+
:core => "Array BasicObject Enumerable Fixnum Float Hash IO Integer Kernel Module Mutex Numeric Object Rational String Symbol Thread Time",
|
18
|
+
:active_record => "ActiveRecord::Base",
|
19
|
+
:data_mapper => "DataMapper::Resource"
|
20
|
+
}
|
21
|
+
@options = Struct.new(:dump_dir, :only, :exclude).new(Dir::tmpdir)
|
22
|
+
|
23
|
+
class Error < StandardError; end
|
24
|
+
|
25
|
+
def config
|
26
|
+
yield @options
|
27
|
+
end
|
28
|
+
|
29
|
+
def start(options = {})
|
30
|
+
opts = merge_options(options)
|
31
|
+
_start mod_names(opts[:only]), mod_names(opts[:exclude])
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
_stop
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def stack_trace
|
41
|
+
@stack_trace || []
|
42
|
+
end
|
43
|
+
|
44
|
+
def select(*only)
|
45
|
+
[].tap do |lines|
|
46
|
+
first, call_stack, only = nil, [], only.flatten.collect{|x| x.split(" ")}.flatten
|
47
|
+
stack_trace.each do |event_info|
|
48
|
+
next unless process?(event_info, only)
|
49
|
+
first ||= event_info
|
50
|
+
if event_info.call?
|
51
|
+
lines << event_info.to_hash(first).merge!(:depth => call_stack.size)
|
52
|
+
call_stack << [lines.size - 1, event_info]
|
53
|
+
elsif event_info.return? && call_stack.last && event_info.matches?(call_stack.last.last)
|
54
|
+
call_stack.pop.tap do |(line, match)|
|
55
|
+
lines[line][:duration] = event_info - match
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def print(*only)
|
63
|
+
puts select(only).collect{ |event|
|
64
|
+
line = " " * event[:depth]
|
65
|
+
line << event[:call]
|
66
|
+
line << " <#{"%.6f" % event[:duration]}>" if event[:duration]
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def dump(path = nil, *only)
|
71
|
+
unless path && path.match(/\.csv$/)
|
72
|
+
path = File.join [path || @options.dump_dir, "stack_events-#{SecureRandom.hex(3)}.csv"].compact
|
73
|
+
end
|
74
|
+
File.expand_path(path).tap do |path|
|
75
|
+
keys = [:event, :file, :line, :singleton, :object, :method, :nsec, :time, :call, :depth, :duration]
|
76
|
+
File.open(path, "w") do |file|
|
77
|
+
file << keys.join(";") + "\n"
|
78
|
+
select(only).each do |event|
|
79
|
+
file << event.values_at(*keys).join(";") + "\n"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def open(path = nil, use_current_stack_trace = false)
|
86
|
+
unless use_current_stack_trace
|
87
|
+
unless path && path.match(/\.csv$/)
|
88
|
+
path = Dir[File.join(path || @options.dump_dir, "stack_events-*.csv")].sort_by{|f| File.mtime(f)}.last
|
89
|
+
path ||= Dir[File.join(path || Dir::tmpdir, "stack_events-*.csv")].sort_by{|f| File.mtime(f)}.last
|
90
|
+
end
|
91
|
+
if path
|
92
|
+
file = File.expand_path(path)
|
93
|
+
else
|
94
|
+
raise Error, "Could not locate StackTracy file"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
index = ui("index.html")
|
99
|
+
|
100
|
+
if use_current_stack_trace || (file && File.exists?(file))
|
101
|
+
events = use_current_stack_trace ? select : StackTracy::EventInfo.to_hashes(File.read(file))
|
102
|
+
erb = ERB.new File.new(ui("index.html.erb")).read
|
103
|
+
File.open(index, "w"){|f| f.write erb.result(binding)}
|
104
|
+
elsif path && path.match(/\.csv$/)
|
105
|
+
raise Error, "Could not locate StackTracy file at #{file}"
|
106
|
+
end
|
107
|
+
|
108
|
+
if File.exists?(index)
|
109
|
+
Launchy.open("file://#{index}")
|
110
|
+
nil
|
111
|
+
else
|
112
|
+
raise Error, "Could not locate StackTracy file"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def merge_options(hash = {})
|
119
|
+
{:only => @options.only, :exclude => @options.exclude}.merge(hash.inject({}){|h, (k, v)| h.merge!(k.to_sym => v)})
|
120
|
+
end
|
121
|
+
|
122
|
+
def mod_names(arg)
|
123
|
+
names = PRESETS.inject([arg || []].flatten.collect(&:to_s).join(" ")){|s, (k, v)| s.gsub k.to_s, v}
|
124
|
+
if names.include?("*")
|
125
|
+
names.split(/\s/).collect do |name|
|
126
|
+
name.include?("*") ? mods_within([constantize(name.gsub("*", ""))]).collect(&:name) : spec
|
127
|
+
end.flatten
|
128
|
+
else
|
129
|
+
names.split(/\s/)
|
130
|
+
end.sort.join(" ")
|
131
|
+
end
|
132
|
+
|
133
|
+
def constantize(name)
|
134
|
+
name.split("::").inject(Kernel){|m, x| m.const_get x}
|
135
|
+
end
|
136
|
+
|
137
|
+
def mods_within(mods, initial_array = nil)
|
138
|
+
(initial_array || mods).tap do |array|
|
139
|
+
mods.each do |mod|
|
140
|
+
mod.constants.each do |c|
|
141
|
+
const = mod.const_get c
|
142
|
+
if !array.include?(const) && (const.is_a?(Class) || const.is_a?(Module)) && const.name.match(/^#{mod.name}/)
|
143
|
+
array << const
|
144
|
+
mods_within([const], array)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def process?(event_info, only)
|
152
|
+
return true if only.empty?
|
153
|
+
only.any?{|x| event_info.matches?(x)}
|
154
|
+
end
|
155
|
+
|
156
|
+
def ui(file)
|
157
|
+
File.expand_path("../../ui/#{file}", __FILE__)
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
require File.expand_path("../../ext/stack_tracy/stack_tracy", __FILE__)
|