nanoc-cli 4.11.13

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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI::CompileListeners
4
+ class Aggregate < Abstract
5
+ def initialize(command_runner:, site:, compiler:)
6
+ @site = site
7
+ @compiler = compiler
8
+ @command_runner = command_runner
9
+
10
+ @listener_classes = self.class.default_listener_classes
11
+ end
12
+
13
+ def start
14
+ setup_listeners
15
+ end
16
+
17
+ def stop
18
+ teardown_listeners
19
+ end
20
+
21
+ def self.default_listener_classes
22
+ [
23
+ Nanoc::CLI::CompileListeners::DiffGenerator,
24
+ Nanoc::CLI::CompileListeners::DebugPrinter,
25
+ Nanoc::CLI::CompileListeners::TimingRecorder,
26
+ Nanoc::CLI::CompileListeners::FileActionPrinter,
27
+ ]
28
+ end
29
+
30
+ protected
31
+
32
+ def setup_listeners
33
+ res = @compiler.run_until_reps_built
34
+ reps = res.fetch(:reps)
35
+
36
+ @listeners =
37
+ @listener_classes
38
+ .select { |klass| klass.enable_for?(@command_runner, @site) }
39
+ .map { |klass| klass.new(reps: reps) }
40
+
41
+ @listeners.each(&:start_safely)
42
+ end
43
+
44
+ def teardown_listeners
45
+ return unless @listeners
46
+
47
+ @listeners.reverse_each(&:stop_safely)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI::CompileListeners
4
+ class DebugPrinter < Abstract
5
+ # @see Listener#enable_for?
6
+ def self.enable_for?(command_runner, _site)
7
+ command_runner.debug?
8
+ end
9
+
10
+ COLOR_MAP = {
11
+ 'compilation' => "\e[31m",
12
+ 'content' => "\e[32m",
13
+ 'filtering' => "\e[33m",
14
+ 'dependency_tracking' => "\e[34m",
15
+ 'phase' => "\e[35m",
16
+ 'stage' => "\e[36m",
17
+ }.freeze
18
+
19
+ # @see Listener#start
20
+ def start
21
+ on(:compilation_started) do |rep|
22
+ log('compilation', "Started compilation of #{rep}")
23
+ end
24
+
25
+ on(:compilation_ended) do |rep|
26
+ log('compilation', "Ended compilation of #{rep}")
27
+ log('compilation', '')
28
+ end
29
+
30
+ on(:compilation_suspended) do |rep, target_rep, snapshot_name|
31
+ log('compilation', "Suspended compilation of #{rep}: depends on #{target_rep}, snapshot #{snapshot_name}")
32
+ end
33
+
34
+ on(:cached_content_used) do |rep|
35
+ log('content', "Used cached compiled content for #{rep} instead of recompiling")
36
+ end
37
+
38
+ on(:snapshot_created) do |rep, snapshot_name|
39
+ log('content', "Snapshot #{snapshot_name} created for #{rep}")
40
+ end
41
+
42
+ on(:filtering_started) do |rep, filter_name|
43
+ log('filtering', "Started filtering #{rep} with #{filter_name}")
44
+ end
45
+
46
+ on(:filtering_ended) do |rep, filter_name|
47
+ log('filtering', "Ended filtering #{rep} with #{filter_name}")
48
+ end
49
+
50
+ on(:dependency_created) do |src, dst|
51
+ log('dependency_tracking', "Dependency created from #{src.inspect} onto #{dst.inspect}")
52
+ end
53
+
54
+ on(:phase_started) do |phase_name, rep|
55
+ log('phase', "Phase started: #{phase_name} (rep: #{rep})")
56
+ end
57
+
58
+ on(:phase_yielded) do |phase_name, rep|
59
+ log('phase', "Phase yielded: #{phase_name} (rep: #{rep})")
60
+ end
61
+
62
+ on(:phase_resumed) do |phase_name, rep|
63
+ log('phase', "Phase resumed: #{phase_name} (rep: #{rep})")
64
+ end
65
+
66
+ on(:phase_ended) do |phase_name, rep|
67
+ log('phase', "Phase ended: #{phase_name} (rep: #{rep})")
68
+ end
69
+
70
+ on(:phase_aborted) do |phase_name, rep|
71
+ log('phase', "Phase aborted: #{phase_name} (rep: #{rep})")
72
+ end
73
+
74
+ on(:stage_started) do |stage_name|
75
+ log('stage', "Stage started: #{stage_name}")
76
+ end
77
+
78
+ on(:stage_ended) do |stage_name|
79
+ log('stage', "Stage ended: #{stage_name}")
80
+ end
81
+
82
+ on(:stage_aborted) do |stage_name|
83
+ log('stage', "Stage aborted: #{stage_name}")
84
+ end
85
+ end
86
+
87
+ def log(progname, msg)
88
+ logger.info(progname) { msg }
89
+ end
90
+
91
+ def logger
92
+ @_logger ||=
93
+ Logger.new($stdout).tap do |l|
94
+ l.formatter = proc do |_severity, datetime, progname, msg|
95
+ "*** #{datetime.strftime('%H:%M:%S.%L')} #{COLOR_MAP[progname]}#{msg}\e[0m\n"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI::CompileListeners
4
+ class DiffGenerator < Abstract
5
+ class Differ
6
+ def initialize(path, str_a, str_b)
7
+ @path = path
8
+ @str_a = str_a
9
+ @str_b = str_b
10
+ end
11
+
12
+ def call
13
+ run
14
+ end
15
+
16
+ private
17
+
18
+ def run
19
+ lines_a = @str_a.lines.map(&:chomp)
20
+ lines_b = @str_b.lines.map(&:chomp)
21
+
22
+ diffs = Diff::LCS.diff(lines_a, lines_b)
23
+
24
+ output = +''
25
+ output << "--- #{@path}\n"
26
+ output << "+++ #{@path}\n"
27
+
28
+ prev_hunk = hunk = nil
29
+ file_length_difference = 0
30
+ diffs.each do |piece|
31
+ begin
32
+ hunk = Diff::LCS::Hunk.new(lines_a, lines_b, piece, 3, file_length_difference)
33
+ file_length_difference = hunk.file_length_difference
34
+
35
+ next unless prev_hunk
36
+ next if hunk.merge(prev_hunk)
37
+
38
+ output << prev_hunk.diff(:unified) << "\n"
39
+ ensure
40
+ prev_hunk = hunk
41
+ end
42
+ end
43
+ last = prev_hunk.diff(:unified)
44
+ output << last << "\n"
45
+
46
+ output
47
+ end
48
+ end
49
+
50
+ # @see Listener#enable_for?
51
+ def self.enable_for?(command_runner, site)
52
+ site.config[:enable_output_diff] || command_runner.options[:diff]
53
+ end
54
+
55
+ # @see Listener#start
56
+ def start
57
+ setup_diffs
58
+
59
+ on(:rep_ready_for_diff) do |raw_path, old_content, new_content|
60
+ generate_diff_for(raw_path, old_content, new_content)
61
+ end
62
+ end
63
+
64
+ # @see Listener#stop
65
+ def stop
66
+ teardown_diffs
67
+ end
68
+
69
+ protected
70
+
71
+ def setup_diffs
72
+ @diff_lock = Mutex.new
73
+ @diff_threads = []
74
+ FileUtils.rm('output.diff') if File.file?('output.diff')
75
+ end
76
+
77
+ def teardown_diffs
78
+ @diff_threads.each(&:join)
79
+ end
80
+
81
+ def generate_diff_for(path, old_content, new_content)
82
+ return if old_content == new_content
83
+
84
+ @diff_threads << Thread.new do
85
+ # Simplify path
86
+ # FIXME: do not depend on working directory
87
+ if path.start_with?(Dir.getwd)
88
+ path = path[(Dir.getwd.size + 1)..path.size]
89
+ end
90
+
91
+ # Generate diff
92
+ diff = Differ.new(path, old_content, new_content).call
93
+
94
+ # Write diff
95
+ @diff_lock.synchronize do
96
+ File.open('output.diff', 'a') { |io| io.write(diff) }
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI::CompileListeners
4
+ class FileActionPrinter < Abstract
5
+ def initialize(reps:)
6
+ @reps = reps
7
+
8
+ @stopwatches = {}
9
+ end
10
+
11
+ # @see Listener#start
12
+ def start
13
+ on(:compilation_started) do |rep|
14
+ @stopwatches[rep] ||= DDMetrics::Stopwatch.new
15
+ @stopwatches[rep].start
16
+ end
17
+
18
+ on(:compilation_suspended) do |rep|
19
+ @stopwatches[rep].stop
20
+ end
21
+
22
+ cached_reps = Set.new
23
+ on(:cached_content_used) do |rep|
24
+ cached_reps << rep
25
+ end
26
+
27
+ on(:rep_write_enqueued) do |rep|
28
+ @stopwatches[rep].stop
29
+ end
30
+
31
+ on(:rep_write_started) do |rep, _raw_path|
32
+ @stopwatches[rep].start
33
+ end
34
+
35
+ on(:rep_write_ended) do |rep, _binary, path, is_created, is_modified|
36
+ @stopwatches[rep].stop
37
+ duration = @stopwatches[rep].duration
38
+
39
+ action =
40
+ if is_created then :create
41
+ elsif is_modified then :update
42
+ elsif cached_reps.include?(rep) then :cached
43
+ else :identical
44
+ end
45
+ level =
46
+ if is_created then :high
47
+ elsif is_modified then :high
48
+ else :low
49
+ end
50
+
51
+ # FIXME: do not depend on working directory
52
+ if path.start_with?(Dir.getwd)
53
+ path = path[(Dir.getwd.size + 1)..path.size]
54
+ end
55
+
56
+ log(level, action, path, duration)
57
+ end
58
+
59
+ on(:file_pruned) do |path|
60
+ Nanoc::CLI::Logger.instance.file(:high, :delete, path)
61
+ end
62
+ end
63
+
64
+ # @see Listener#stop
65
+ def stop
66
+ @reps.reject(&:compiled?).each do |rep|
67
+ raw_paths = rep.raw_paths.values.flatten.uniq
68
+ raw_paths.each do |raw_path|
69
+ log(:low, :skip, raw_path, nil)
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def log(level, action, path, duration)
77
+ Nanoc::CLI::Logger.instance.file(level, action, path, duration)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI::CompileListeners
4
+ class TimingRecorder < Abstract
5
+ attr_reader :stages_summary
6
+ attr_reader :phases_summary
7
+ attr_reader :outdatedness_rules_summary
8
+ attr_reader :filters_summary
9
+
10
+ # @see Listener#enable_for?
11
+ def self.enable_for?(_command_runner, _site)
12
+ Nanoc::CLI.verbosity >= 1
13
+ end
14
+
15
+ # @param [Enumerable<Nanoc::Core::ItemRep>] reps
16
+ def initialize(reps:)
17
+ @reps = reps
18
+
19
+ @stages_summary = DDMetrics::Summary.new
20
+ @phases_summary = DDMetrics::Summary.new
21
+ @outdatedness_rules_summary = DDMetrics::Summary.new
22
+ @filters_summary = DDMetrics::Summary.new
23
+ @load_stores_summary = DDMetrics::Summary.new
24
+ end
25
+
26
+ # @see Listener#start
27
+ def start
28
+ on(:stage_ran) do |duration, klass|
29
+ @stages_summary.observe(duration, name: klass.to_s.sub(/.*::/, ''))
30
+ end
31
+
32
+ on(:outdatedness_rule_ran) do |duration, klass|
33
+ @outdatedness_rules_summary.observe(duration, name: klass.to_s.sub(/.*::/, ''))
34
+ end
35
+
36
+ filter_stopwatches = {}
37
+
38
+ on(:filtering_started) do |rep, _filter_name|
39
+ stopwatch_stack = filter_stopwatches.fetch(rep) { filter_stopwatches[rep] = [] }
40
+ stopwatch_stack << DDMetrics::Stopwatch.new
41
+ stopwatch_stack.last.start
42
+ end
43
+
44
+ on(:filtering_ended) do |rep, filter_name|
45
+ stopwatch = filter_stopwatches.fetch(rep).pop
46
+ stopwatch.stop
47
+
48
+ @filters_summary.observe(stopwatch.duration, name: filter_name.to_s)
49
+ end
50
+
51
+ on(:store_loaded) do |duration, klass|
52
+ @load_stores_summary.observe(duration, name: klass.to_s)
53
+ end
54
+
55
+ on(:compilation_suspended) do |rep, _target_rep, _snapshot_name|
56
+ filter_stopwatches.fetch(rep).each(&:stop)
57
+ end
58
+
59
+ on(:compilation_started) do |rep|
60
+ filter_stopwatches.fetch(rep, []).each(&:start)
61
+ end
62
+
63
+ setup_phase_notifications
64
+ end
65
+
66
+ # @see Listener#stop
67
+ def stop
68
+ print_profiling_feedback
69
+ end
70
+
71
+ protected
72
+
73
+ def setup_phase_notifications
74
+ stopwatches = {}
75
+
76
+ on(:phase_started) do |phase_name, rep|
77
+ stopwatch = stopwatches[[phase_name, rep]] = DDMetrics::Stopwatch.new
78
+ stopwatch.start
79
+ end
80
+
81
+ on(:phase_ended) do |phase_name, rep|
82
+ stopwatch = stopwatches[[phase_name, rep]]
83
+ stopwatch.stop
84
+
85
+ @phases_summary.observe(stopwatch.duration, name: phase_name)
86
+ end
87
+
88
+ on(:phase_yielded) do |phase_name, rep|
89
+ stopwatch = stopwatches[[phase_name, rep]]
90
+ stopwatch.stop
91
+ end
92
+
93
+ on(:phase_resumed) do |phase_name, rep|
94
+ # It probably looks weird that a phase can be resumed even though it was not suspended earlier. This can happen when compilation is suspended, where you’d get the sequence started -> suspended -> started -> resumed.
95
+ stopwatch = stopwatches[[phase_name, rep]]
96
+ stopwatch.start unless stopwatch.running?
97
+ end
98
+
99
+ on(:phase_aborted) do |phase_name, rep|
100
+ stopwatch = stopwatches[[phase_name, rep]]
101
+ stopwatch.stop if stopwatch.running?
102
+
103
+ @phases_summary.observe(stopwatch.duration, name: phase_name)
104
+ end
105
+ end
106
+
107
+ def table_for_summary(name, summary)
108
+ headers = [name.to_s, 'count', 'min', '.50', '.90', '.95', 'max', 'tot']
109
+
110
+ rows = summary.map do |label, stats|
111
+ name = label.fetch(:name)
112
+
113
+ count = stats.count
114
+ min = stats.min
115
+ p50 = stats.quantile(0.50)
116
+ p90 = stats.quantile(0.90)
117
+ p95 = stats.quantile(0.95)
118
+ tot = stats.sum
119
+ max = stats.max
120
+
121
+ [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, summary)
128
+ headers = [name.to_s, 'tot']
129
+
130
+ rows = summary.map do |label, stats|
131
+ name = label.fetch(:name)
132
+ [name, "#{format('%4.2f', stats.sum)}s"]
133
+ end
134
+
135
+ [headers] + rows
136
+ end
137
+
138
+ def print_profiling_feedback
139
+ print_table_for_summary(:filters, @filters_summary)
140
+ print_table_for_summary(:phases, @phases_summary) if Nanoc::CLI.verbosity >= 2
141
+ print_table_for_summary_duration(:stages, @stages_summary) if Nanoc::CLI.verbosity >= 2
142
+ print_table_for_summary(:outdatedness_rules, @outdatedness_rules_summary) if Nanoc::CLI.verbosity >= 2
143
+ print_table_for_summary_duration(:load_stores, @load_stores_summary) if Nanoc::CLI.verbosity >= 2
144
+ print_ddmemoize_metrics if Nanoc::CLI.verbosity >= 2
145
+ end
146
+
147
+ def print_table_for_summary(name, summary)
148
+ return unless summary.any?
149
+
150
+ puts
151
+ print_table(table_for_summary(name, summary))
152
+ end
153
+
154
+ def print_table_for_summary_duration(name, summary)
155
+ return unless summary.any?
156
+
157
+ puts
158
+ print_table(table_for_summary_durations(name, summary))
159
+ end
160
+
161
+ def print_ddmemoize_metrics
162
+ puts
163
+ DDMemoize.print_metrics
164
+ end
165
+
166
+ def print_table(rows)
167
+ puts DDMetrics::Table.new(rows).to_s
168
+ end
169
+ end
170
+ end