nanoc-cli 4.11.13

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