liquidprof 0.0.1
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 +4 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +21 -0
- data/LICENSE +20 -0
- data/README.md +94 -0
- data/Rakefile +24 -0
- data/TODO +13 -0
- data/add_liquid_tests.sh +10 -0
- data/cli.rb +8 -0
- data/lib/liquidprof.rb +5 -0
- data/lib/liquidprof/ascii_reporter.rb +73 -0
- data/lib/liquidprof/profiler.rb +193 -0
- data/lib/liquidprof/reporter.rb +73 -0
- data/lib/liquidprof/version.rb +3 -0
- data/liquidprof.gemspec +24 -0
- data/test/liquid/test/test_helper.rb +6 -0
- data/test/test.rb +60 -0
- data/test/test_helper.rb +122 -0
- metadata +132 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
liquidprof (0.0.1)
|
5
|
+
liquid (~> 2.5)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
liquid (2.5.0)
|
11
|
+
minitest (5.0.6)
|
12
|
+
rake (10.1.0)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
bundler (~> 1.3)
|
19
|
+
liquidprof!
|
20
|
+
minitest (~> 5.0)
|
21
|
+
rake
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Florian Weingarten
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
LiquidProf
|
2
|
+
==========
|
3
|
+
|
4
|
+
LiquidProf is a simple profiler for the [Liquid](https://github.com/Shopify/liquid)
|
5
|
+
templating language, inspired by [rblineprof](https://github.com/tmm1/rblineprof).
|
6
|
+
|
7
|
+
This is work in progress and not really ready to use!
|
8
|
+
|
9
|
+
Installation
|
10
|
+
------------
|
11
|
+
* ```gem install liquidprof``` (https://rubygems.org/gems/liquidprof)
|
12
|
+
|
13
|
+
Profiling
|
14
|
+
---------
|
15
|
+
|
16
|
+
Wrapping your ```parse()``` and ```render()``` calls in a
|
17
|
+
```LiquidProf::Profiler.profile``` block will profile the containing
|
18
|
+
templates and return a Profiler object which encapsulates the profiling
|
19
|
+
results of each of the templates.
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
prof = LiquidProf::Profiler.profile(10) do
|
23
|
+
template1 = Liquid::Template.parse(str1)
|
24
|
+
output1 = template1.render()
|
25
|
+
|
26
|
+
template2 = Liquid::Template.new
|
27
|
+
template2.parse(str2)
|
28
|
+
output2 = template2.render()
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
A bit more explicit:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
prof = LiquidProf::Profiler.start
|
36
|
+
template = Liquid::Template.parse(str)
|
37
|
+
prof.profile(template, 10)
|
38
|
+
LiquidProf::Profiler.stop
|
39
|
+
```
|
40
|
+
|
41
|
+
Reporting
|
42
|
+
---------
|
43
|
+
```ruby
|
44
|
+
puts LiquidProf::AsciiReporter.report(prof, template)
|
45
|
+
```
|
46
|
+
|
47
|
+
will generate a report like this:
|
48
|
+
|
49
|
+
```
|
50
|
+
+----------------------+
|
51
|
+
1 | 1x, 0.05ms, 0B | {% assign user = "flo" %}
|
52
|
+
2 | |
|
53
|
+
3 | 1x, 0.05ms, 3B | Hello {{ user }},
|
54
|
+
4 | |
|
55
|
+
5 | | this is an example Liquid template.
|
56
|
+
6 | |
|
57
|
+
7 | 1x, 0.00ms, 0B | {% comment %}
|
58
|
+
8 | 0x, 0.00ms, 0B | {% assign user = "florian" %}
|
59
|
+
9 | | {% endcomment %}
|
60
|
+
10 | |
|
61
|
+
11 | 1x, 4.97ms, 0B | {% capture test %}
|
62
|
+
12 | 1x, 0.02ms, 0B | {% assign c = 0 %}
|
63
|
+
13 | 1x, 4.92ms, 1.53K | {% for i in (1..10) %}
|
64
|
+
14 | 10x, 0.16ms, 60B | {% if i % 2 == 0 %} even {% endif %}
|
65
|
+
15 | 10x, 4.45ms, 1.34K | {% for j in (i..10) %}
|
66
|
+
16 | 55x, 0.09ms, 100B | {% increment c %}: {{ i }} + {{ j }} = {{ i + j }}
|
67
|
+
| 55x, 0.55ms, 56B |
|
68
|
+
| 55x, 0.51ms, 65B |
|
69
|
+
| 55x, 0.52ms, 56B |
|
70
|
+
17 | | {% endfor %}
|
71
|
+
18 | | {% endfor %}
|
72
|
+
19 | | {% endcapture %}
|
73
|
+
20 | |
|
74
|
+
21 | 1x, 0.01ms, 1B | {{ c }}: {{ test }}
|
75
|
+
| 1x, 0.01ms, 1.54K |
|
76
|
+
22 | |
|
77
|
+
23 | 1x, 0.01ms, 3B | Bye {{ user }}.
|
78
|
+
+----------------------+
|
79
|
+
5.19ms, 1.60K
|
80
|
+
```
|
81
|
+
|
82
|
+
TODO
|
83
|
+
----
|
84
|
+
* Fancy HTML reporter, possibly using [highlight.js](http://softwaremaniacs.org/soft/highlight/en/description/)
|
85
|
+
|
86
|
+
Tests
|
87
|
+
-----
|
88
|
+
* Run ```./add_liquid_tests.sh <path_to_liquid_git_repo>```
|
89
|
+
* Run ```rake test``` to run LiquidProf tests
|
90
|
+
* Run ```rake test_liquid``` to run Liquid tests with LiquidProf profiling enabled
|
91
|
+
|
92
|
+
Bugs
|
93
|
+
----
|
94
|
+
* LiquidProf is not thread-safe in some situations. Enabling/disabling the profiler in one thread will do so for all other threads as well.
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rubygems/package_task'
|
3
|
+
|
4
|
+
gemspec = eval(File.read('liquidprof.gemspec'))
|
5
|
+
Gem::PackageTask.new(gemspec) do |pkg|
|
6
|
+
pkg.gem_spec = gemspec
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Build the gem and release it to rubygems.org"
|
10
|
+
task :release => :gem do
|
11
|
+
sh "gem push pkg/liquidprof-#{gemspec.version}.gem"
|
12
|
+
end
|
13
|
+
|
14
|
+
Rake::TestTask.new(:test) do |t|
|
15
|
+
t.libs << 'test'
|
16
|
+
end
|
17
|
+
|
18
|
+
Rake::TestTask.new(:test_liquid) do |t|
|
19
|
+
t.test_files = FileList['test/liquid/test/liquid/*_test.rb']
|
20
|
+
t.libs << 'test/liquid/test'
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Run tests"
|
24
|
+
task :default => :test
|
data/TODO
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
* Tests
|
2
|
+
* All tags are counted correctly
|
3
|
+
* n times in loops
|
4
|
+
* 0 times in anevaluated branches
|
5
|
+
* 0 times in comment and raw
|
6
|
+
* inherited tags are counted correctly
|
7
|
+
* output byte length is correct and adds up
|
8
|
+
* min < average < max always holds
|
9
|
+
* stddev correct?
|
10
|
+
|
11
|
+
* Package as gem
|
12
|
+
* Travis CI
|
13
|
+
* Rubygems
|
data/add_liquid_tests.sh
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
if [ "$#" -ne 1 ] || ! [ -d "$1" ]; then
|
3
|
+
echo "Usage: $0 <directory to liquid source tree>" >&2
|
4
|
+
exit 1
|
5
|
+
fi
|
6
|
+
|
7
|
+
LIQUID=$(readlink -e $1)
|
8
|
+
ln -s $LIQUID/lib test/liquid/lib
|
9
|
+
ln -s $LIQUID/test/liquid test/liquid/test/liquid
|
10
|
+
ln -s $LIQUID/test/test_helper.rb test/liquid/test/liquid_test_helper.rb
|
data/cli.rb
ADDED
data/lib/liquidprof.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module LiquidProf
|
2
|
+
class AsciiReporter < Reporter
|
3
|
+
def format_node_stats(stats, skip_times=false)
|
4
|
+
[
|
5
|
+
skip_times ? nil : ("%dx" % stats[:calls][:avg]),
|
6
|
+
"%.2fms" % (1000.0 * stats[:times][:avg]),
|
7
|
+
format_bytes(stats[:lengths][:avg])
|
8
|
+
].compact.join(", ")
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.report(prof)
|
12
|
+
AsciiReporter.new(prof).report()
|
13
|
+
end
|
14
|
+
|
15
|
+
def report
|
16
|
+
output = ""
|
17
|
+
@prof.templates.each do |template|
|
18
|
+
output << report_template(template)
|
19
|
+
output << "\n"
|
20
|
+
output << "\n" if @prof.templates.length > 1
|
21
|
+
end
|
22
|
+
output
|
23
|
+
end
|
24
|
+
|
25
|
+
def report_template(template)
|
26
|
+
summarize_stats(template)
|
27
|
+
sidenotes = Hash.new{ Array.new }
|
28
|
+
res = render_source(template) do |node, line|
|
29
|
+
sidenotes[line] += [ @prof.stats[node] ]
|
30
|
+
node.raw_markup
|
31
|
+
end
|
32
|
+
sidenotes = sidenotes.inject(Array.new) do |a,(k,v)|
|
33
|
+
a[k] = v.map{ |stats| format_node_stats(stats) }
|
34
|
+
a
|
35
|
+
end
|
36
|
+
|
37
|
+
output = []
|
38
|
+
res.each_with_index do |line, i|
|
39
|
+
(sidenotes[i] || [""]).each_with_index do |note, j|
|
40
|
+
output << ((j == 0) ? [ (i+1).to_s, note, line] : [ "", note, "" ])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
output << [ "", format_node_stats(@prof.stats[template.root], true), "" ]
|
45
|
+
format_table(output)
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_table(lines)
|
49
|
+
max_width = []
|
50
|
+
|
51
|
+
lines.each do |line|
|
52
|
+
line.each_with_index do |column, i|
|
53
|
+
next if i == line.length-1
|
54
|
+
max_width[i] = [ max_width[i], column.length ].compact.max
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
lines.each do |line|
|
59
|
+
line.each_with_index do |column, i|
|
60
|
+
next if i == line.length-1
|
61
|
+
line[i] = column.rjust(max_width[i])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
table = ""
|
66
|
+
table << [ " " * (max_width[0]+2), "-" * (max_width[1]+4), "" ].join("+") + "\n"
|
67
|
+
table << lines[0..-2].map{ |line| line.join(" | ") }.join("\n") + "\n"
|
68
|
+
table << [ " " * (max_width[0]+2), "-" * (max_width[1]+4), "" ].join("+") + "\n"
|
69
|
+
table << lines.last.join(" ").rjust(max_width[1])
|
70
|
+
table
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module LiquidProf
|
2
|
+
class Profiler
|
3
|
+
attr_accessor :stats, :templates
|
4
|
+
|
5
|
+
def stats_init(root)
|
6
|
+
if root.class == Liquid::Template
|
7
|
+
return stats_init(root.root)
|
8
|
+
end
|
9
|
+
|
10
|
+
Profiler.dfs(root) do |node, pos|
|
11
|
+
next unless pos == :pre
|
12
|
+
next if node.class == String
|
13
|
+
stats_init_node(node)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def profile(template, iterations=1, *args)
|
18
|
+
output = ""
|
19
|
+
@templates << template
|
20
|
+
iterations.times do
|
21
|
+
stats_init(template)
|
22
|
+
output = template.render!(*args)
|
23
|
+
end
|
24
|
+
output
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_raw_markup
|
28
|
+
Profiler.unhook(:create_variable, Liquid::Block)
|
29
|
+
Profiler.unhook(:initialize, Liquid::Tag)
|
30
|
+
Profiler.unhook(:end_tag, Liquid::Block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_raw_markup
|
34
|
+
Profiler.hook(:create_variable, Liquid::Block) do |node, method, args|
|
35
|
+
var = method.(*args)
|
36
|
+
var.instance_variable_set :@raw_markup, args.first
|
37
|
+
var.class.class_eval { attr_reader :raw_markup }
|
38
|
+
var
|
39
|
+
end
|
40
|
+
|
41
|
+
Profiler.hook(:initialize, Liquid::Tag) do |node, method, args|
|
42
|
+
method.(*args)
|
43
|
+
node.instance_variable_set :@raw_markup, "{% " + (args[0].strip + " " + args[1].strip).strip + " %}"
|
44
|
+
node.class.class_eval { attr_reader :raw_markup }
|
45
|
+
end
|
46
|
+
|
47
|
+
Profiler.hook(:end_tag, Liquid::Block) do |node, method, args|
|
48
|
+
node.instance_variable_set :@raw_markup_end, "{% #{node.block_delimiter} %}"
|
49
|
+
node.class.class_eval { attr_reader :raw_markup_end }
|
50
|
+
method.(*args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_profiling(tags)
|
55
|
+
Profiler.unhook(:render, tags)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_profiling(tags)
|
59
|
+
Profiler.hook(:render, tags) do |node, method, args|
|
60
|
+
output = nil
|
61
|
+
start = Time.now
|
62
|
+
output = method.(*args)
|
63
|
+
time = Time.now - start
|
64
|
+
stats_inc(node, time, output.to_s.length)
|
65
|
+
output
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def initialize(tags)
|
72
|
+
@stats = {}
|
73
|
+
@templates = []
|
74
|
+
add_profiling(tags)
|
75
|
+
add_raw_markup()
|
76
|
+
end
|
77
|
+
|
78
|
+
def stats_init_node(node)
|
79
|
+
@stats[node] ||= {}
|
80
|
+
[:calls, :times, :lengths].each do |field|
|
81
|
+
@stats[node][field] ||= {}
|
82
|
+
@stats[node][field][:raw] ||= []
|
83
|
+
@stats[node][field][:raw] << 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def stats_inc(node, time, length)
|
88
|
+
return unless @stats[node]
|
89
|
+
@stats[node][:calls][:raw][-1] += 1
|
90
|
+
@stats[node][:times][:raw][-1] += time
|
91
|
+
@stats[node][:lengths][:raw][-1] += length
|
92
|
+
end
|
93
|
+
|
94
|
+
class << self
|
95
|
+
def profiler
|
96
|
+
@@profiler
|
97
|
+
end
|
98
|
+
|
99
|
+
def start
|
100
|
+
@@profiler ||= Profiler.new(Profiler.all_tags() + [Liquid::Variable])
|
101
|
+
end
|
102
|
+
|
103
|
+
def stop
|
104
|
+
return unless @@profiler
|
105
|
+
tags = Profiler.all_tags() + [Liquid::Variable]
|
106
|
+
@@profiler.remove_profiling(tags)
|
107
|
+
@@profiler.remove_raw_markup()
|
108
|
+
prof = @@profiler
|
109
|
+
@@profiler = nil
|
110
|
+
prof
|
111
|
+
end
|
112
|
+
|
113
|
+
def profile(iterations=1, &block)
|
114
|
+
prof = Profiler.start
|
115
|
+
|
116
|
+
hook(:parse, Liquid::Template) do |template, method, args|
|
117
|
+
method.(*args)
|
118
|
+
end
|
119
|
+
|
120
|
+
hook(:render, Liquid::Template) do |template, method, args|
|
121
|
+
output = ""
|
122
|
+
prof.templates << template
|
123
|
+
iterations.times do
|
124
|
+
prof.stats_init(template)
|
125
|
+
output = method.(*args)
|
126
|
+
end
|
127
|
+
output
|
128
|
+
end
|
129
|
+
|
130
|
+
yield
|
131
|
+
|
132
|
+
unhook(:parse, Liquid::Template)
|
133
|
+
unhook(:render, Liquid::Template)
|
134
|
+
Profiler.stop
|
135
|
+
end
|
136
|
+
|
137
|
+
def all_tags
|
138
|
+
ObjectSpace.each_object(Class).select { |klass| klass <= Liquid::Tag }
|
139
|
+
end
|
140
|
+
|
141
|
+
def dfs(root, &block)
|
142
|
+
block.yield(root, :pre)
|
143
|
+
if root.respond_to?(:nodelist) && root.nodelist
|
144
|
+
root.nodelist.each do |child|
|
145
|
+
dfs(child, &block)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
block.yield(root, :post)
|
149
|
+
end
|
150
|
+
|
151
|
+
def hook(method_name, tags, &block)
|
152
|
+
[tags].flatten.each do |tag|
|
153
|
+
tag.class_exec(block, tag) do |block, tag|
|
154
|
+
hooked_name = LiquidProf::Profiler.hooked(method_name, tag)
|
155
|
+
unhooked_name = LiquidProf::Profiler.unhooked(method_name, tag)
|
156
|
+
|
157
|
+
define_method(hooked_name) do |*args|
|
158
|
+
owner = self.class.instance_method(method_name).owner
|
159
|
+
if method_name != :render || (self.class == owner && owner == tag)
|
160
|
+
block.yield(self, method(unhooked_name), args)
|
161
|
+
else
|
162
|
+
send(unhooked_name, *args)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
alias_method unhooked_name, method_name
|
167
|
+
alias_method method_name, hooked_name
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def unhook(method_name, tags)
|
173
|
+
[tags].flatten.each do |tag|
|
174
|
+
tag.class_eval do |tag|
|
175
|
+
if method_defined?(LiquidProf::Profiler.hooked(method_name, tag))
|
176
|
+
alias_method method_name, LiquidProf::Profiler.unhooked(method_name, tag)
|
177
|
+
remove_method LiquidProf::Profiler.hooked(method_name, tag)
|
178
|
+
remove_method LiquidProf::Profiler.unhooked(method_name, tag)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def hooked(method_name, tag)
|
185
|
+
"#{method_name}_#{tag}_hooked"
|
186
|
+
end
|
187
|
+
|
188
|
+
def unhooked(method_name, tag)
|
189
|
+
"#{method_name}_#{tag}_unhooked"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module LiquidProf
|
2
|
+
class Reporter
|
3
|
+
attr_reader :prof
|
4
|
+
|
5
|
+
def initialize(prof)
|
6
|
+
@prof = prof
|
7
|
+
end
|
8
|
+
|
9
|
+
def summarize_stats(template)
|
10
|
+
Profiler.dfs(template.root) do |node, pos|
|
11
|
+
next unless pos == :pre
|
12
|
+
next unless prof.stats.key?(node)
|
13
|
+
node_summarize_stats(prof.stats[node])
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def format_bytes(bytes)
|
19
|
+
units = ["B", "K", "M"]
|
20
|
+
|
21
|
+
if bytes.to_i < 1024
|
22
|
+
exponent = 0
|
23
|
+
else
|
24
|
+
exponent = (Math.log(bytes)/Math.log(1024)).to_i
|
25
|
+
bytes /= 1024 ** [exponent, units.size].min
|
26
|
+
end
|
27
|
+
|
28
|
+
if exponent == 0
|
29
|
+
"#{bytes.to_i}#{units[exponent]}"
|
30
|
+
else
|
31
|
+
"%.2f%s" % [ bytes, units[exponent] ]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def node_summarize_stats(stats)
|
38
|
+
[:calls, :times, :lengths].each do |field|
|
39
|
+
raw = stats[field][:raw]
|
40
|
+
stats[field][:avg] = mu = avg(raw)
|
41
|
+
stats[field][:max] = raw.max || 0
|
42
|
+
stats[field][:min] = raw.min || 0
|
43
|
+
stats[field][:dev] = raw.length == 1 ? 0.0 : Math.sqrt(raw.inject(0){ |s,i| s + (i - mu)**2 } / (raw.length-1).to_f)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def avg(array)
|
48
|
+
array.length > 0 ? (array.inject(0){ |s,i| s + i }.to_f / array.length.to_f) : 0.0
|
49
|
+
end
|
50
|
+
|
51
|
+
def render_source(template)
|
52
|
+
res = ""
|
53
|
+
line = 0
|
54
|
+
Profiler.dfs(template.root) do |node, pos|
|
55
|
+
next if node.class == Liquid::Document
|
56
|
+
if pos == :pre
|
57
|
+
res << if node.class == String
|
58
|
+
line += node.scan(/\r?\n/).length
|
59
|
+
node
|
60
|
+
else
|
61
|
+
yield(node, line)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
if node.respond_to?(:raw_markup_end)
|
65
|
+
res << node.raw_markup_end
|
66
|
+
next
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
res.split(/\r?\n/)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/liquidprof.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'liquidprof/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "liquidprof"
|
7
|
+
spec.version = LiquidProf::VERSION
|
8
|
+
spec.authors = ["Florian Weingarten"]
|
9
|
+
spec.email = ["flo@hackvalue.de"]
|
10
|
+
spec.description = %q{Liquid profiler}
|
11
|
+
spec.summary = %q{Performance profiler for the Liquid templating language}
|
12
|
+
spec.homepage = "https://github.com/fw42/liquidprof"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "liquid", "~> 2.5"
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
24
|
+
end
|
data/test/test.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "test_helper.rb"
|
2
|
+
|
3
|
+
class SomeTestClass
|
4
|
+
def foo(*args)
|
5
|
+
return *args
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class LiquidProfTest < Minitest::Test
|
10
|
+
def test_hooking_and_unhooking_works
|
11
|
+
called = false
|
12
|
+
LiquidProf::Profiler.hook(:foo, SomeTestClass) do |node, method, args|
|
13
|
+
called = true
|
14
|
+
args.reverse!
|
15
|
+
method.(*args)
|
16
|
+
end
|
17
|
+
obj = SomeTestClass.new
|
18
|
+
assert_equal [3,2,1], obj.foo(1,2,3)
|
19
|
+
assert called
|
20
|
+
LiquidProf::Profiler.unhook(:foo, SomeTestClass)
|
21
|
+
assert_equal [1,2,3], obj.foo(1,2,3)
|
22
|
+
end
|
23
|
+
|
24
|
+
EXAMPLES = [
|
25
|
+
[ Liquid::Variable, "{{ 'foo' }}", 3 ],
|
26
|
+
[ Liquid::Assign, "{% assign foo = 'bar' %}", 0 ],
|
27
|
+
[ Liquid::Variable, "{% assign foo = 'bar' %} {{ foo }}", 3 ],
|
28
|
+
[ Liquid::Assign, "{% assign foo = 'bar' %} {{ foo }}", 0 ],
|
29
|
+
[ Liquid::Raw, "{% raw %} {{ 'foo' }} {% endraw %}", 13 ],
|
30
|
+
[ Liquid::Variable, "{% raw %} {{ 'foo' }} {% endraw %}", 0, false ],
|
31
|
+
]
|
32
|
+
|
33
|
+
EXAMPLES.each_with_index do |example, index|
|
34
|
+
define_method "test_#{example[0]}_#{index}_stats_are_sound" do
|
35
|
+
prof = profile(example[1])
|
36
|
+
stats = get_stats_for_tag(prof, example[0])
|
37
|
+
assert example[3] == false ? 0 : 1, stats.length
|
38
|
+
assert_stats 1, 1, example[2], stats
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def get_stats_for_tag(prof, tag)
|
45
|
+
stats = []
|
46
|
+
LiquidProf::Profiler.dfs(prof.templates.first.root) do |node, pos|
|
47
|
+
if node.class == tag
|
48
|
+
stats << node
|
49
|
+
end
|
50
|
+
end
|
51
|
+
stats.uniq.map{ |node| prof.stats[node] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def assert_stats(nodes, calls, length, stats)
|
55
|
+
stats.each do |stat|
|
56
|
+
assert_equal calls, stat[:calls][:raw][0]
|
57
|
+
assert_equal length, stat[:lengths][:raw][0]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'minitest/unit'
|
4
|
+
require 'minitest/pride'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', File.dirname(__FILE__))
|
7
|
+
require "liquidprof"
|
8
|
+
|
9
|
+
def assert_profile_result(expected, template_str)
|
10
|
+
assert_equal expected, profile(template_str)
|
11
|
+
end
|
12
|
+
|
13
|
+
def profile(template_str)
|
14
|
+
profs = [*1..5].map do |i|
|
15
|
+
assert_profile_syntax_doesnt_matter(template_str, i)
|
16
|
+
end
|
17
|
+
assert_profile_iterations_dont_matter(*profs)
|
18
|
+
assert_raw_markup_present(profs.first.templates.first.root)
|
19
|
+
profs.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def assert_raw_markup_present(root)
|
23
|
+
if root.class != Liquid::Document && (root.is_a?(Liquid::Tag) || root.is_a?(Liquid::Variable))
|
24
|
+
assert root.instance_variable_defined?(:@raw_markup)
|
25
|
+
end
|
26
|
+
|
27
|
+
if root.class != Liquid::Document && root.is_a?(Liquid::Block)
|
28
|
+
assert root.instance_variable_defined?(:@raw_markup_end)
|
29
|
+
end
|
30
|
+
|
31
|
+
if root.respond_to?(:nodelist) && root.nodelist
|
32
|
+
root.nodelist.each do |child|
|
33
|
+
assert_raw_markup_present(child)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert_stats_equal(prof1, prof2)
|
39
|
+
assert_equal prof1.stats.length, prof2.stats.length
|
40
|
+
keys1 = prof1.stats.keys.sort_by{ |node| node.class.to_s }
|
41
|
+
keys2 = prof2.stats.keys.sort_by{ |node| node.class.to_s }
|
42
|
+
assert_equal keys1.map(&:class).map(&:to_s), keys2.map(&:class).map(&:to_s)
|
43
|
+
|
44
|
+
keys1.each_index do |i|
|
45
|
+
k1, k2 = keys1[i], keys2[i]
|
46
|
+
assert_equal prof1.stats[k1][:times][:raw].length, prof2.stats[k2][:times][:raw].length
|
47
|
+
assert_equal prof1.stats[k1][:calls], prof2.stats[k2][:calls]
|
48
|
+
assert_equal prof1.stats[k1][:lengths], prof2.stats[k2][:lengths]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def assert_profile_iterations_dont_matter(*profs)
|
53
|
+
profs.each_index do |i|
|
54
|
+
profs[i] = profs[i].stats.values.sort_by{ |h| h.to_s }
|
55
|
+
end
|
56
|
+
|
57
|
+
profs.first.each_index do |i|
|
58
|
+
0.upto(profs.length-2) do |j|
|
59
|
+
(j+1).upto(profs.length-1) do |k|
|
60
|
+
[ :avg, :min, :max ].each do |field|
|
61
|
+
assert_equal profs[j][i][:calls][field], profs[k][i][:calls][field]
|
62
|
+
assert_equal profs[j][i][:lengths][field], profs[k][i][:lengths][field]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def assert_profile_syntax_doesnt_matter(template_str, iterations=1, template_iterations=2)
|
70
|
+
profs = []
|
71
|
+
templates = []
|
72
|
+
outputs = []
|
73
|
+
|
74
|
+
profs << LiquidProf::Profiler.start
|
75
|
+
template_iterations.times do
|
76
|
+
templates << Liquid::Template.new
|
77
|
+
templates.last.parse(template_str)
|
78
|
+
outputs << profs.last.profile(templates.last, iterations)
|
79
|
+
end
|
80
|
+
LiquidProf::Profiler.stop
|
81
|
+
|
82
|
+
profs << LiquidProf::Profiler.start
|
83
|
+
template_iterations.times do
|
84
|
+
templates << Liquid::Template.parse(template_str)
|
85
|
+
outputs << profs.last.profile(templates.last, iterations)
|
86
|
+
end
|
87
|
+
LiquidProf::Profiler.stop
|
88
|
+
|
89
|
+
profs << LiquidProf::Profiler.profile(iterations) do
|
90
|
+
template_iterations.times do
|
91
|
+
templates << Liquid::Template.new
|
92
|
+
templates.last.parse(template_str)
|
93
|
+
outputs << templates.last.render
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
profs << LiquidProf::Profiler.profile(iterations) do
|
98
|
+
template_iterations.times do
|
99
|
+
templates << Liquid::Template.parse(template_str)
|
100
|
+
outputs << templates.last.render
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
assert_equal 1, outputs.uniq.length
|
105
|
+
assert_equal profs.length * template_iterations, outputs.length
|
106
|
+
profs.each do |prof|
|
107
|
+
assert_equal template_iterations, prof.templates.length
|
108
|
+
assert_equal template_iterations, prof.stats.keys.select{ |key|
|
109
|
+
key.class == Liquid::Document
|
110
|
+
}.length
|
111
|
+
end
|
112
|
+
|
113
|
+
assert_equal profs, profs.compact
|
114
|
+
0.upto(profs.length-2) do |i|
|
115
|
+
(i+1).upto(profs.length-1) do |j|
|
116
|
+
assert profs[i].__id__ != profs[j].__id__
|
117
|
+
assert_stats_equal(profs[i], profs[j])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
return profs.first
|
122
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: liquidprof
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Florian Weingarten
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: liquid
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.5'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.5'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.3'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.3'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '5.0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '5.0'
|
78
|
+
description: Liquid profiler
|
79
|
+
email:
|
80
|
+
- flo@hackvalue.de
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- .travis.yml
|
87
|
+
- Gemfile
|
88
|
+
- Gemfile.lock
|
89
|
+
- LICENSE
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- TODO
|
93
|
+
- add_liquid_tests.sh
|
94
|
+
- cli.rb
|
95
|
+
- lib/liquidprof.rb
|
96
|
+
- lib/liquidprof/ascii_reporter.rb
|
97
|
+
- lib/liquidprof/profiler.rb
|
98
|
+
- lib/liquidprof/reporter.rb
|
99
|
+
- lib/liquidprof/version.rb
|
100
|
+
- liquidprof.gemspec
|
101
|
+
- test/liquid/test/test_helper.rb
|
102
|
+
- test/test.rb
|
103
|
+
- test/test_helper.rb
|
104
|
+
homepage: https://github.com/fw42/liquidprof
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ! '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ! '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.8.23
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: Performance profiler for the Liquid templating language
|
129
|
+
test_files:
|
130
|
+
- test/liquid/test/test_helper.rb
|
131
|
+
- test/test.rb
|
132
|
+
- test/test_helper.rb
|