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.
@@ -0,0 +1,4 @@
1
+ examples/
2
+ test/liquid/lib
3
+ test/liquid/test/liquid
4
+ test/liquid/test/liquid_test_helper.rb
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ script: bundle exec rake
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - ruby-head
7
+ - rbx-19mode
8
+ - jruby-19mode
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -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.
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ require "liquidprof"
2
+
3
+ prof = LiquidProf::Profiler.profile(1) do
4
+ input = STDIN.read
5
+ Liquid::Template.parse(input).render
6
+ end
7
+
8
+ puts LiquidProf::AsciiReporter.report(prof)
@@ -0,0 +1,5 @@
1
+ require "liquid"
2
+ require "liquidprof/version"
3
+ require "liquidprof/profiler"
4
+ require "liquidprof/reporter"
5
+ require "liquidprof/ascii_reporter"
@@ -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
@@ -0,0 +1,3 @@
1
+ module LiquidProf
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', File.dirname(__FILE__))
2
+
3
+ require "liquidprof"
4
+ LiquidProf::Profiler.start
5
+
6
+ require File.expand_path("liquid_test_helper.rb", File.dirname(__FILE__))
@@ -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
@@ -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