nanoc 4.7.0 → 4.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/NEWS.md +12 -1
  4. data/README.md +1 -1
  5. data/lib/nanoc/base/entities/directed_graph.rb +16 -0
  6. data/lib/nanoc/base/entities/identifiable_collection.rb +24 -8
  7. data/lib/nanoc/base/entities/outdatedness_reasons.rb +5 -0
  8. data/lib/nanoc/base/errors.rb +11 -6
  9. data/lib/nanoc/base/memoization.rb +6 -23
  10. data/lib/nanoc/base/services/compiler/phases/abstract.rb +3 -3
  11. data/lib/nanoc/base/services/compiler/phases/cache.rb +1 -3
  12. data/lib/nanoc/base/services/compiler/phases/mark_done.rb +1 -3
  13. data/lib/nanoc/base/services/compiler/phases/recalculate.rb +1 -3
  14. data/lib/nanoc/base/services/compiler/phases/resume.rb +1 -3
  15. data/lib/nanoc/base/services/compiler/phases/write.rb +1 -3
  16. data/lib/nanoc/base/services/filter.rb +15 -0
  17. data/lib/nanoc/base/services/item_rep_selector.rb +1 -1
  18. data/lib/nanoc/base/services/outdatedness_checker.rb +3 -1
  19. data/lib/nanoc/base/services/outdatedness_rule.rb +7 -0
  20. data/lib/nanoc/base/services/outdatedness_rules.rb +16 -0
  21. data/lib/nanoc/cli/commands/compile.rb +12 -411
  22. data/lib/nanoc/cli/commands/compile_listeners/abstract.rb +30 -0
  23. data/lib/nanoc/cli/commands/compile_listeners/debug_printer.rb +34 -0
  24. data/lib/nanoc/cli/commands/compile_listeners/diff_generator.rb +91 -0
  25. data/lib/nanoc/cli/commands/compile_listeners/file_action_printer.rb +61 -0
  26. data/lib/nanoc/cli/commands/compile_listeners/stack_prof_profiler.rb +22 -0
  27. data/lib/nanoc/cli/commands/compile_listeners/timing_recorder.rb +174 -0
  28. data/lib/nanoc/filters/xsl.rb +2 -0
  29. data/lib/nanoc/version.rb +1 -1
  30. data/spec/nanoc/base/directed_graph_spec.rb +54 -0
  31. data/spec/nanoc/base/errors/dependency_cycle_spec.rb +32 -0
  32. data/spec/nanoc/base/filter_spec.rb +36 -0
  33. data/spec/nanoc/base/services/compiler/phases/abstract_spec.rb +23 -11
  34. data/spec/nanoc/base/services/outdatedness_rules_spec.rb +41 -0
  35. data/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb +1 -1
  36. data/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb +124 -54
  37. data/spec/nanoc/cli/commands/compile_spec.rb +1 -1
  38. data/spec/nanoc/regressions/gh_1040_spec.rb +1 -1
  39. data/spec/nanoc/regressions/gh_924_spec.rb +89 -0
  40. data/test/base/test_compiler.rb +1 -1
  41. data/test/base/test_memoization.rb +6 -0
  42. data/test/cli/commands/test_compile.rb +2 -2
  43. metadata +12 -3
@@ -0,0 +1,30 @@
1
+ module Nanoc::CLI::Commands::CompileListeners
2
+ class Abstract
3
+ def initialize(*); end
4
+
5
+ def self.enable_for?(command_runner) # rubocop:disable Lint/UnusedMethodArgument
6
+ true
7
+ end
8
+
9
+ def start
10
+ raise NotImplementedError, "Subclasses of #{self.class} must implement #start"
11
+ end
12
+
13
+ def stop; end
14
+
15
+ def start_safely
16
+ start
17
+ @_started = true
18
+ end
19
+
20
+ def stop_safely
21
+ stop if @_started
22
+ @_started = false
23
+ end
24
+
25
+ def on(sym)
26
+ # TODO: clean up on stop
27
+ Nanoc::Int::NotificationCenter.on(sym, self) { |*args| yield(*args) }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ module Nanoc::CLI::Commands::CompileListeners
2
+ class DebugPrinter < Abstract
3
+ # @see Listener#enable_for?
4
+ def self.enable_for?(command_runner)
5
+ command_runner.debug?
6
+ end
7
+
8
+ # @see Listener#start
9
+ def start
10
+ Nanoc::Int::NotificationCenter.on(:compilation_started) do |rep|
11
+ puts "*** Started compilation of #{rep.inspect}"
12
+ end
13
+ Nanoc::Int::NotificationCenter.on(:compilation_ended) do |rep|
14
+ puts "*** Ended compilation of #{rep.inspect}"
15
+ puts
16
+ end
17
+ Nanoc::Int::NotificationCenter.on(:compilation_suspended) do |rep, e|
18
+ puts "*** Suspended compilation of #{rep.inspect}: #{e.message}"
19
+ end
20
+ Nanoc::Int::NotificationCenter.on(:cached_content_used) do |rep|
21
+ puts "*** Used cached compiled content for #{rep.inspect} instead of recompiling"
22
+ end
23
+ Nanoc::Int::NotificationCenter.on(:filtering_started) do |rep, filter_name|
24
+ puts "*** Started filtering #{rep.inspect} with #{filter_name}"
25
+ end
26
+ Nanoc::Int::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
27
+ puts "*** Ended filtering #{rep.inspect} with #{filter_name}"
28
+ end
29
+ Nanoc::Int::NotificationCenter.on(:dependency_created) do |src, dst|
30
+ puts "*** Dependency created from #{src.inspect} onto #{dst.inspect}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ module Nanoc::CLI::Commands::CompileListeners
2
+ class DiffGenerator < Abstract
3
+ # @see Listener#enable_for?
4
+ def self.enable_for?(command_runner)
5
+ command_runner.site.config[:enable_output_diff]
6
+ end
7
+
8
+ # @see Listener#start
9
+ def start
10
+ require 'tempfile'
11
+ setup_diffs
12
+ old_contents = {}
13
+ Nanoc::Int::NotificationCenter.on(:will_write_rep, self) do |rep, path|
14
+ old_contents[rep] = File.file?(path) ? File.read(path) : nil
15
+ end
16
+ Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, binary, path, _is_created, _is_modified|
17
+ unless binary
18
+ new_contents = File.file?(path) ? File.read(path) : nil
19
+ if old_contents[rep] && new_contents
20
+ generate_diff_for(path, old_contents[rep], new_contents)
21
+ end
22
+ old_contents.delete(rep)
23
+ end
24
+ end
25
+ end
26
+
27
+ # @see Listener#stop
28
+ def stop
29
+ super
30
+
31
+ Nanoc::Int::NotificationCenter.remove(:will_write_rep, self)
32
+ Nanoc::Int::NotificationCenter.remove(:rep_written, self)
33
+
34
+ teardown_diffs
35
+ end
36
+
37
+ protected
38
+
39
+ def setup_diffs
40
+ @diff_lock = Mutex.new
41
+ @diff_threads = []
42
+ FileUtils.rm('output.diff') if File.file?('output.diff')
43
+ end
44
+
45
+ def teardown_diffs
46
+ @diff_threads.each(&:join)
47
+ end
48
+
49
+ def generate_diff_for(path, old_content, new_content)
50
+ return if old_content == new_content
51
+
52
+ @diff_threads << Thread.new do
53
+ # Generate diff
54
+ diff = diff_strings(old_content, new_content)
55
+ diff.sub!(/^--- .*/, '--- ' + path)
56
+ diff.sub!(/^\+\+\+ .*/, '+++ ' + path)
57
+
58
+ # Write diff
59
+ @diff_lock.synchronize do
60
+ File.open('output.diff', 'a') { |io| io.write(diff) }
61
+ end
62
+ end
63
+ end
64
+
65
+ def diff_strings(a, b)
66
+ require 'open3'
67
+
68
+ # Create files
69
+ Tempfile.open('old') do |old_file|
70
+ Tempfile.open('new') do |new_file|
71
+ # Write files
72
+ old_file.write(a)
73
+ old_file.flush
74
+ new_file.write(b)
75
+ new_file.flush
76
+
77
+ # Diff
78
+ cmd = ['diff', '-u', old_file.path, new_file.path]
79
+ Open3.popen3(*cmd) do |_stdin, stdout, _stderr|
80
+ result = stdout.read
81
+ return (result == '' ? nil : result)
82
+ end
83
+ end
84
+ end
85
+ rescue Errno::ENOENT
86
+ warn 'Failed to run `diff`, so no diff with the previously compiled ' \
87
+ 'content will be available.'
88
+ nil
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,61 @@
1
+ module Nanoc::CLI::Commands::CompileListeners
2
+ class FileActionPrinter < Abstract
3
+ def initialize(reps:)
4
+ @start_times = {}
5
+ @acc_durations = {}
6
+
7
+ @reps = reps
8
+ end
9
+
10
+ # @see Listener#start
11
+ def start
12
+ Nanoc::Int::NotificationCenter.on(:compilation_started, self) do |rep|
13
+ @start_times[rep] = Time.now
14
+ @acc_durations[rep] ||= 0.0
15
+ end
16
+
17
+ Nanoc::Int::NotificationCenter.on(:compilation_suspended, self) do |rep|
18
+ @acc_durations[rep] += Time.now - @start_times[rep]
19
+ end
20
+
21
+ Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, _binary, path, is_created, is_modified|
22
+ @acc_durations[rep] += Time.now - @start_times[rep]
23
+ duration = @acc_durations[rep]
24
+
25
+ action =
26
+ if is_created then :create
27
+ elsif is_modified then :update
28
+ else :identical
29
+ end
30
+ level =
31
+ if is_created then :high
32
+ elsif is_modified then :high
33
+ else :low
34
+ end
35
+ log(level, action, path, duration)
36
+ end
37
+ end
38
+
39
+ # @see Listener#stop
40
+ def stop
41
+ super
42
+
43
+ Nanoc::Int::NotificationCenter.remove(:compilation_started, self)
44
+ Nanoc::Int::NotificationCenter.remove(:compilation_suspended, self)
45
+ Nanoc::Int::NotificationCenter.remove(:rep_written, self)
46
+
47
+ @reps.reject(&:compiled?).each do |rep|
48
+ raw_paths = rep.raw_paths.values.flatten.uniq
49
+ raw_paths.each do |raw_path|
50
+ log(:low, :skip, raw_path, nil)
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def log(level, action, path, duration)
58
+ Nanoc::CLI::Logger.instance.file(level, action, path, duration)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ module Nanoc::CLI::Commands::CompileListeners
2
+ class StackProfProfiler < Abstract
3
+ PROFILE_FILE = 'tmp/stackprof_profile'.freeze
4
+
5
+ # @see Listener#enable_for?
6
+ def self.enable_for?(command_runner)
7
+ command_runner.options.fetch(:profile, false)
8
+ end
9
+
10
+ # @see Listener#start
11
+ def start
12
+ require 'stackprof'
13
+ StackProf.start(mode: :cpu)
14
+ end
15
+
16
+ # @see Listener#stop
17
+ def stop
18
+ StackProf.stop
19
+ StackProf.results(PROFILE_FILE)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,174 @@
1
+ module Nanoc::CLI::Commands::CompileListeners
2
+ class TimingRecorder < Abstract
3
+ attr_reader :telemetry
4
+
5
+ # @see Listener#enable_for?
6
+ def self.enable_for?(command_runner)
7
+ command_runner.options.fetch(:verbose, false)
8
+ end
9
+
10
+ # @param [Enumerable<Nanoc::Int::ItemRep>] reps
11
+ def initialize(reps:)
12
+ @reps = reps
13
+ @telemetry = Nanoc::Telemetry.new
14
+ end
15
+
16
+ # @see Listener#start
17
+ def start
18
+ stage_stopwatch = Nanoc::Telemetry::Stopwatch.new
19
+
20
+ on(:stage_started) do |_stage_name|
21
+ stage_stopwatch.start
22
+ end
23
+
24
+ on(:stage_ended) do |stage_name|
25
+ stage_stopwatch.stop
26
+ @telemetry.summary(:stages).observe(stage_stopwatch.duration, stage_name.to_s)
27
+ stage_stopwatch = Nanoc::Telemetry::Stopwatch.new
28
+ end
29
+
30
+ outdatedness_rule_stopwatches = {}
31
+
32
+ on(:outdatedness_rule_started) do |klass, obj|
33
+ stopwatches = outdatedness_rule_stopwatches.fetch(klass) { outdatedness_rule_stopwatches[klass] = {} }
34
+ stopwatch = stopwatches.fetch(obj) { stopwatches[obj] = Nanoc::Telemetry::Stopwatch.new }
35
+ stopwatch.start
36
+ end
37
+
38
+ on(:outdatedness_rule_ended) do |klass, obj|
39
+ stopwatches = outdatedness_rule_stopwatches.fetch(klass)
40
+ stopwatch = stopwatches.fetch(obj)
41
+ stopwatch.stop
42
+
43
+ @telemetry.summary(:outdatedness_rules).observe(stopwatch.duration, klass.to_s.sub(/.*::/, ''))
44
+ end
45
+
46
+ filter_stopwatches = {}
47
+
48
+ on(:filtering_started) do |rep, _filter_name|
49
+ stopwatch_stack = filter_stopwatches.fetch(rep) { filter_stopwatches[rep] = [] }
50
+ stopwatch_stack << Nanoc::Telemetry::Stopwatch.new
51
+ stopwatch_stack.last.start
52
+ end
53
+
54
+ on(:filtering_ended) do |rep, filter_name|
55
+ stopwatch = filter_stopwatches.fetch(rep).pop
56
+ stopwatch.stop
57
+
58
+ @telemetry.summary(:filters).observe(stopwatch.duration, filter_name.to_s)
59
+ end
60
+
61
+ on(:compilation_suspended) do |rep, _exception|
62
+ filter_stopwatches.fetch(rep).each(&:stop)
63
+ end
64
+
65
+ on(:compilation_started) do |rep|
66
+ filter_stopwatches.fetch(rep, []).each(&:start)
67
+ end
68
+
69
+ phase_stopwatches = {}
70
+
71
+ on(:phase_started) do |phase_name, rep|
72
+ stopwatches = phase_stopwatches.fetch(rep) { phase_stopwatches[rep] = {} }
73
+ stopwatches[phase_name] = Nanoc::Telemetry::Stopwatch.new.tap(&:start)
74
+ end
75
+
76
+ on(:phase_ended) do |phase_name, rep|
77
+ stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
78
+ stopwatch.stop
79
+
80
+ @telemetry.summary(:phases).observe(stopwatch.duration, phase_name)
81
+ end
82
+
83
+ on(:phase_yielded) do |phase_name, rep|
84
+ stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
85
+ stopwatch.stop
86
+ end
87
+
88
+ on(:phase_resumed) do |phase_name, rep|
89
+ stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
90
+ stopwatch.start if stopwatch.stopped?
91
+ end
92
+
93
+ on(:phase_aborted) do |phase_name, rep|
94
+ stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
95
+ stopwatch.stop if stopwatch.running?
96
+
97
+ @telemetry.summary(:phases).observe(stopwatch.duration, phase_name)
98
+ end
99
+ end
100
+
101
+ # @see Listener#stop
102
+ def stop
103
+ print_profiling_feedback
104
+ super
105
+ end
106
+
107
+ protected
108
+
109
+ def table_for_summary(name)
110
+ headers = [name.to_s, 'count', 'min', '.50', '.90', '.95', 'max', 'tot']
111
+
112
+ rows = @telemetry.summary(name).map do |filter_name, summary|
113
+ count = summary.count
114
+ min = summary.min
115
+ p50 = summary.quantile(0.50)
116
+ p90 = summary.quantile(0.90)
117
+ p95 = summary.quantile(0.95)
118
+ tot = summary.sum
119
+ max = summary.max
120
+
121
+ [filter_name, count.to_s] + [min, p50, p90, p95, max, tot].map { |r| "#{format('%4.2f', r)}s" }
122
+ end
123
+
124
+ [headers] + rows
125
+ end
126
+
127
+ def table_for_summary_durations(name)
128
+ headers = [name.to_s, 'tot']
129
+
130
+ rows = @telemetry.summary(:stages).map do |stage_name, summary|
131
+ [stage_name, "#{format('%4.2f', summary.sum)}s"]
132
+ end
133
+
134
+ [headers] + rows
135
+ end
136
+
137
+ def print_profiling_feedback
138
+ print_table_for_summary(:filters)
139
+ print_table_for_summary(:phases) if Nanoc::CLI.verbosity >= 2
140
+ print_table_for_summary_duration(:stages) if Nanoc::CLI.verbosity >= 2
141
+ print_table_for_summary(:outdatedness_rules) if Nanoc::CLI.verbosity >= 2
142
+ end
143
+
144
+ def print_table_for_summary(name)
145
+ return if @telemetry.summary(name).empty?
146
+
147
+ puts
148
+ print_table(table_for_summary(name))
149
+ end
150
+
151
+ def print_table_for_summary_duration(name)
152
+ return if @telemetry.summary(name).empty?
153
+
154
+ puts
155
+ print_table(table_for_summary_durations(name))
156
+ end
157
+
158
+ def print_table(table)
159
+ lengths = table.transpose.map { |r| r.map(&:size).max }
160
+
161
+ print_row(table[0], lengths)
162
+
163
+ puts "#{'─' * lengths[0]}─┼─#{lengths[1..-1].map { |length| '─' * length }.join('───')}"
164
+
165
+ table[1..-1].each { |row| print_row(row, lengths) }
166
+ end
167
+
168
+ def print_row(row, lengths)
169
+ values = row.zip(lengths).map { |text, length| text.rjust length }
170
+
171
+ puts values[0] + ' │ ' + values[1..-1].join(' ')
172
+ end
173
+ end
174
+ end
@@ -5,6 +5,8 @@ module Nanoc::Filters
5
5
 
6
6
  requires 'nokogiri'
7
7
 
8
+ always_outdated
9
+
8
10
  # Runs the item content through an [XSLT](http://www.w3.org/TR/xslt)
9
11
  # stylesheet using [Nokogiri](http://nokogiri.org/).
10
12
  #
data/lib/nanoc/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Nanoc
2
2
  # The current Nanoc version.
3
- VERSION = '4.7.0'.freeze
3
+ VERSION = '4.7.1'.freeze
4
4
  end
@@ -0,0 +1,54 @@
1
+ describe Nanoc::Int::DirectedGraph do
2
+ subject(:graph) { described_class.new([1, 2, 3]) }
3
+
4
+ describe '#any_cycle' do
5
+ subject { graph.any_cycle }
6
+
7
+ context 'no cycles' do
8
+ it { is_expected.to be_nil }
9
+ end
10
+
11
+ context 'one cycle without head' do
12
+ before do
13
+ graph.add_edge(1, 2)
14
+ graph.add_edge(2, 1)
15
+ end
16
+
17
+ it { is_expected.to eq([1, 2]) }
18
+ end
19
+
20
+ context 'one cycle with head' do
21
+ before do
22
+ graph.add_edge(1, 2)
23
+ graph.add_edge(2, 3)
24
+ graph.add_edge(3, 2)
25
+ end
26
+
27
+ it { is_expected.to eq([2, 3]) }
28
+ end
29
+
30
+ context 'large cycle' do
31
+ before do
32
+ graph.add_edge(1, 2)
33
+ graph.add_edge(2, 3)
34
+ graph.add_edge(3, 4)
35
+ graph.add_edge(4, 5)
36
+ graph.add_edge(5, 1)
37
+ end
38
+
39
+ it { is_expected.to eq([1, 2, 3, 4, 5]) }
40
+ end
41
+
42
+ context 'large cycle with head' do
43
+ before do
44
+ graph.add_edge(1, 2)
45
+ graph.add_edge(2, 3)
46
+ graph.add_edge(3, 4)
47
+ graph.add_edge(4, 5)
48
+ graph.add_edge(5, 2)
49
+ end
50
+
51
+ it { is_expected.to eq([2, 3, 4, 5]) }
52
+ end
53
+ end
54
+ end