stack_tracy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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__)
|