hone 0.1.0

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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +8 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +201 -0
  6. data/Rakefile +10 -0
  7. data/examples/.hone/harness.rb +41 -0
  8. data/examples/README.md +22 -0
  9. data/examples/allocation_patterns.rb +66 -0
  10. data/examples/cpu_patterns.rb +50 -0
  11. data/examples/jit_patterns.rb +69 -0
  12. data/exe/hone +7 -0
  13. data/lib/hone/adapters/base.rb +35 -0
  14. data/lib/hone/adapters/fasterer.rb +38 -0
  15. data/lib/hone/adapters/rubocop_performance.rb +85 -0
  16. data/lib/hone/analyzer.rb +258 -0
  17. data/lib/hone/cli.rb +247 -0
  18. data/lib/hone/config.rb +93 -0
  19. data/lib/hone/correlator.rb +250 -0
  20. data/lib/hone/exit_codes.rb +10 -0
  21. data/lib/hone/finding.rb +64 -0
  22. data/lib/hone/finding_filter.rb +57 -0
  23. data/lib/hone/formatters/base.rb +25 -0
  24. data/lib/hone/formatters/filterable.rb +31 -0
  25. data/lib/hone/formatters/github.rb +71 -0
  26. data/lib/hone/formatters/json.rb +75 -0
  27. data/lib/hone/formatters/junit.rb +154 -0
  28. data/lib/hone/formatters/sarif.rb +179 -0
  29. data/lib/hone/formatters/tsv.rb +49 -0
  30. data/lib/hone/harness.rb +57 -0
  31. data/lib/hone/harness_generator.rb +128 -0
  32. data/lib/hone/harness_runner.rb +172 -0
  33. data/lib/hone/method_map.rb +140 -0
  34. data/lib/hone/patterns/README.md +174 -0
  35. data/lib/hone/patterns/array_compact.rb +105 -0
  36. data/lib/hone/patterns/array_include_set.rb +34 -0
  37. data/lib/hone/patterns/base.rb +90 -0
  38. data/lib/hone/patterns/block_to_proc.rb +109 -0
  39. data/lib/hone/patterns/bsearch_vs_find.rb +80 -0
  40. data/lib/hone/patterns/chars_map_ord.rb +42 -0
  41. data/lib/hone/patterns/chars_to_variable.rb +136 -0
  42. data/lib/hone/patterns/chars_to_variable_tainted.rb +136 -0
  43. data/lib/hone/patterns/constant_regexp.rb +74 -0
  44. data/lib/hone/patterns/count_vs_size.rb +35 -0
  45. data/lib/hone/patterns/divmod.rb +92 -0
  46. data/lib/hone/patterns/dynamic_ivar.rb +44 -0
  47. data/lib/hone/patterns/dynamic_ivar_get.rb +33 -0
  48. data/lib/hone/patterns/each_with_index.rb +116 -0
  49. data/lib/hone/patterns/each_with_object.rb +63 -0
  50. data/lib/hone/patterns/flatten_once.rb +28 -0
  51. data/lib/hone/patterns/gsub_to_tr.rb +48 -0
  52. data/lib/hone/patterns/hash_each_key.rb +41 -0
  53. data/lib/hone/patterns/hash_each_value.rb +31 -0
  54. data/lib/hone/patterns/hash_keys_include.rb +30 -0
  55. data/lib/hone/patterns/hash_merge_bang.rb +33 -0
  56. data/lib/hone/patterns/hash_values_include.rb +31 -0
  57. data/lib/hone/patterns/inject_sum.rb +48 -0
  58. data/lib/hone/patterns/kernel_loop.rb +27 -0
  59. data/lib/hone/patterns/lazy_ivar.rb +39 -0
  60. data/lib/hone/patterns/map_compact.rb +32 -0
  61. data/lib/hone/patterns/map_flatten.rb +31 -0
  62. data/lib/hone/patterns/map_select_chain.rb +32 -0
  63. data/lib/hone/patterns/parallel_assignment.rb +127 -0
  64. data/lib/hone/patterns/positive_predicate.rb +27 -0
  65. data/lib/hone/patterns/range_include.rb +34 -0
  66. data/lib/hone/patterns/redundant_string_chars.rb +82 -0
  67. data/lib/hone/patterns/regexp_match.rb +126 -0
  68. data/lib/hone/patterns/reverse_each.rb +30 -0
  69. data/lib/hone/patterns/reverse_first.rb +40 -0
  70. data/lib/hone/patterns/select_count.rb +32 -0
  71. data/lib/hone/patterns/select_first.rb +31 -0
  72. data/lib/hone/patterns/select_map.rb +32 -0
  73. data/lib/hone/patterns/shuffle_first.rb +30 -0
  74. data/lib/hone/patterns/slice_with_length.rb +48 -0
  75. data/lib/hone/patterns/sort_by_first.rb +31 -0
  76. data/lib/hone/patterns/sort_by_last.rb +31 -0
  77. data/lib/hone/patterns/sort_first.rb +52 -0
  78. data/lib/hone/patterns/sort_last.rb +30 -0
  79. data/lib/hone/patterns/sort_reverse.rb +53 -0
  80. data/lib/hone/patterns/string_casecmp.rb +54 -0
  81. data/lib/hone/patterns/string_chars_each.rb +56 -0
  82. data/lib/hone/patterns/string_concat_in_loop.rb +116 -0
  83. data/lib/hone/patterns/string_delete_prefix.rb +53 -0
  84. data/lib/hone/patterns/string_delete_suffix.rb +53 -0
  85. data/lib/hone/patterns/string_empty.rb +64 -0
  86. data/lib/hone/patterns/string_end_with.rb +81 -0
  87. data/lib/hone/patterns/string_shovel.rb +75 -0
  88. data/lib/hone/patterns/string_start_with.rb +80 -0
  89. data/lib/hone/patterns/taint_tracking_base.rb +230 -0
  90. data/lib/hone/patterns/times_map.rb +38 -0
  91. data/lib/hone/patterns/uniq_by.rb +32 -0
  92. data/lib/hone/patterns/yield_vs_block.rb +72 -0
  93. data/lib/hone/profilers/base.rb +162 -0
  94. data/lib/hone/profilers/factory.rb +31 -0
  95. data/lib/hone/profilers/memory_profiler.rb +213 -0
  96. data/lib/hone/profilers/stackprof.rb +99 -0
  97. data/lib/hone/profilers/vernier.rb +147 -0
  98. data/lib/hone/reporter.rb +371 -0
  99. data/lib/hone/scanner.rb +75 -0
  100. data/lib/hone/suggestion_generator.rb +23 -0
  101. data/lib/hone/version.rb +5 -0
  102. data/lib/hone.rb +108 -0
  103. data/logo.png +0 -0
  104. data/sig/hone.rbs +4 -0
  105. metadata +176 -0
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ # Orchestrates the full analysis pipeline:
5
+ # Scanner -> MethodMap -> Profiler -> Correlator -> Reporter
6
+ #
7
+ # @example Basic usage (static analysis only)
8
+ # analyzer = Hone::Analyzer.new("app.rb")
9
+ # result = analyzer.run
10
+ # puts result.output
11
+ #
12
+ # @example With profile correlation
13
+ # analyzer = Hone::Analyzer.new("app.rb", profile: "profile.json")
14
+ # result = analyzer.run
15
+ #
16
+ # @example Directory analysis
17
+ # analyzer = Hone::Analyzer.new("lib/", format: :json)
18
+ # result = analyzer.run
19
+ #
20
+ class Analyzer
21
+ # Default directory for cached profile files
22
+ PROFILE_DIR = "tmp/hone"
23
+
24
+ # Result of analysis containing findings, output, and exit code
25
+ Result = Data.define(:findings, :output, :exit_code)
26
+
27
+ # @param path [String] File or directory to analyze
28
+ # @param profile [String, nil] Path to StackProf JSON profile
29
+ # @param memory_profile [String, nil] Path to MemoryProfiler JSON profile
30
+ # @param format [Symbol] Output format: :text, :json, :github
31
+ # @param color [Boolean, nil] Force color output (nil = auto-detect)
32
+ # @param quiet [Boolean] Minimal output
33
+ # @param verbose [Boolean] Extended output with pattern details
34
+ # @param top [Integer, nil] Show only top N findings
35
+ # @param hot_only [Boolean] Only show HOT findings
36
+ # @param show_cold [Boolean] Include COLD findings
37
+ # @param fail_on [Symbol] Exit non-zero for: :hot, :warm, :any, :none
38
+ # @param diff [String, nil] Git ref to compare against for changed files
39
+ # @param baseline [String, nil] Path to baseline JSON file to suppress findings
40
+ # @param rails [Boolean] Enable Rails-specific analysis
41
+ def initialize(path, profile: nil, memory_profile: nil, format: :text, color: nil, quiet: false, verbose: false,
42
+ top: nil, hot_only: false, show_cold: false, fail_on: :hot, diff: nil, baseline: nil, rails: false)
43
+ @path = path
44
+ @profile_path = profile || auto_detect_profile(:cpu)
45
+ @memory_profile_path = memory_profile || auto_detect_profile(:memory)
46
+ @format = format.to_sym
47
+ @color = color
48
+ @quiet = quiet
49
+ @verbose = verbose
50
+ @top = top
51
+ @hot_only = hot_only
52
+ @show_cold = show_cold
53
+ @fail_on = fail_on.to_sym
54
+ @diff = diff
55
+ @baseline = baseline
56
+ @rails = rails
57
+ end
58
+
59
+ # Run the analysis pipeline
60
+ # @return [Result] Analysis result with findings, output, and exit code
61
+ def run
62
+ validate_input!
63
+
64
+ # Step 1: Scan for patterns
65
+ findings = scan_path
66
+
67
+ # Step 2: Build method map for correlation
68
+ method_map = build_method_map
69
+
70
+ # Step 3: Load profile data (if available)
71
+ profiles = load_profiles
72
+
73
+ # Step 4: Correlate findings with profile
74
+ correlator = Correlator.new(
75
+ method_map: method_map,
76
+ cpu_profile: profiles[:cpu],
77
+ memory_profile: profiles[:memory]
78
+ )
79
+ correlated = correlator.correlate(findings)
80
+
81
+ # Step 5: Apply filtering
82
+ filtered = apply_filters(correlated)
83
+
84
+ # Step 6: Generate output
85
+ output = generate_output(filtered)
86
+
87
+ # Step 7: Calculate exit code
88
+ exit_code = calculate_exit_code(filtered)
89
+
90
+ Result.new(findings: filtered, output: output, exit_code: exit_code)
91
+ rescue Error => e
92
+ Result.new(findings: [], output: "Error: #{e.message}", exit_code: ExitCodes::ERROR)
93
+ rescue => e
94
+ Result.new(findings: [], output: "Error: #{e.message}\n#{e.backtrace.first(5).join("\n")}", exit_code: ExitCodes::ERROR)
95
+ end
96
+
97
+ private
98
+
99
+ def validate_input!
100
+ raise Error, "Path not found: #{@path}" unless File.exist?(@path)
101
+
102
+ raise Error, "Profile not found: #{@profile_path}" if @profile_path && !File.exist?(@profile_path)
103
+
104
+ raise Error, "Baseline not found: #{@baseline}" if @baseline && !File.exist?(@baseline)
105
+
106
+ raise Error, "Memory profile not found: #{@memory_profile_path}" if @memory_profile_path && !File.exist?(@memory_profile_path)
107
+
108
+ unless %i[text json github sarif junit tsv].include?(@format)
109
+ raise Error, "Invalid format: #{@format}. Use text, json, github, sarif, junit, or tsv."
110
+ end
111
+
112
+ return if %i[hot warm any none].include?(@fail_on)
113
+
114
+ raise Error, "Invalid fail_on: #{@fail_on}. Use hot, warm, any, or none."
115
+ end
116
+
117
+ def scan_path
118
+ scanner = Scanner.new(rails: @rails)
119
+
120
+ if File.directory?(@path)
121
+ scanner.scan_directory(@path)
122
+ else
123
+ scanner.scan_file(@path)
124
+ end
125
+ end
126
+
127
+ def build_method_map
128
+ method_map = MethodMap.new
129
+
130
+ if File.directory?(@path)
131
+ Dir.glob(File.join(@path, "**/*.rb")).each do |file|
132
+ method_map.add_file(file)
133
+ end
134
+ else
135
+ method_map.add_file(@path)
136
+ end
137
+
138
+ method_map
139
+ end
140
+
141
+ def load_profiles
142
+ {
143
+ cpu: load_cpu_profile,
144
+ memory: load_memory_profile
145
+ }
146
+ end
147
+
148
+ def load_cpu_profile
149
+ Profilers::Factory.create(@profile_path)
150
+ end
151
+
152
+ def load_memory_profile
153
+ return nil unless @memory_profile_path
154
+
155
+ Profilers::MemoryProfiler.new(@memory_profile_path)
156
+ end
157
+
158
+ def apply_filters(findings)
159
+ FindingFilter.new(findings, filter_options).apply
160
+ end
161
+
162
+ def filter_options
163
+ {
164
+ diff: @diff,
165
+ baseline: @baseline,
166
+ hot_only: @hot_only,
167
+ show_cold: @show_cold,
168
+ profile_path: @profile_path,
169
+ top: @top
170
+ }
171
+ end
172
+
173
+ def generate_output(findings)
174
+ case @format
175
+ when :json
176
+ Formatters::JSON.new(findings, reporter_options).format
177
+ when :github
178
+ Formatters::GitHub.new(findings, reporter_options).format
179
+ when :sarif
180
+ Formatters::SARIF.new(findings, reporter_options).format
181
+ when :junit
182
+ Formatters::JUnit.new(findings, reporter_options).format
183
+ when :tsv
184
+ Formatters::TSV.new(findings, reporter_options).format
185
+ else
186
+ Reporter.new(findings, reporter_options).report
187
+ end
188
+ end
189
+
190
+ def reporter_options
191
+ {
192
+ show_cold: @show_cold,
193
+ quiet: @quiet,
194
+ verbose: @verbose,
195
+ color: determine_color,
196
+ profile_path: @profile_path,
197
+ memory_profile_path: @memory_profile_path,
198
+ file_path: @path,
199
+ mode: @profile_path ? "correlated" : "static"
200
+ }
201
+ end
202
+
203
+ def determine_color
204
+ return @color unless @color.nil?
205
+
206
+ # Auto-detect: color if TTY and NO_COLOR not set
207
+ $stdout.tty? && !ENV["NO_COLOR"]
208
+ end
209
+
210
+ def calculate_exit_code(findings)
211
+ return ExitCodes::SUCCESS if findings.empty?
212
+
213
+ case @fail_on
214
+ when :none
215
+ ExitCodes::SUCCESS
216
+ when :any
217
+ ExitCodes::HOT
218
+ when :hot
219
+ has_hot = findings.any? { |f| f.priority == :hot }
220
+ has_hot ? ExitCodes::HOT : ExitCodes::SUCCESS
221
+ when :warm
222
+ has_hot = findings.any? { |f| f.priority == :hot }
223
+ has_warm = findings.any? { |f| f.priority == :warm }
224
+
225
+ if has_hot
226
+ ExitCodes::HOT
227
+ elsif has_warm
228
+ ExitCodes::WARM
229
+ else
230
+ ExitCodes::SUCCESS
231
+ end
232
+ else
233
+ ExitCodes::SUCCESS
234
+ end
235
+ end
236
+
237
+ def auto_detect_profile(type)
238
+ filename = case type
239
+ when :cpu then "cpu_profile.json"
240
+ when :memory then "memory_profile.json"
241
+ end
242
+
243
+ path = File.join(PROFILE_DIR, filename)
244
+
245
+ if File.exist?(path)
246
+ metadata_path = File.join(PROFILE_DIR, "metadata.json")
247
+ if File.exist?(metadata_path)
248
+ metadata = JSON.parse(File.read(metadata_path))
249
+ generated = metadata["generated_at"]
250
+ warn "Using cached #{type} profile from #{path} (generated: #{generated})"
251
+ else
252
+ warn "Using cached #{type} profile from #{path}"
253
+ end
254
+ path
255
+ end
256
+ end
257
+ end
258
+ end
data/lib/hone/cli.rb ADDED
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "harness"
5
+ require_relative "harness_generator"
6
+ require_relative "harness_runner"
7
+
8
+ module Hone
9
+ # Command-line interface for Hone Ruby performance analyzer.
10
+ #
11
+ # @example Basic usage
12
+ # hone analyze FILE # Static analysis (no profile)
13
+ # hone analyze FILE --profile FILE # Correlation mode (prioritized)
14
+ # hone analyze DIR # Analyze directory
15
+ #
16
+ # @example Output control
17
+ # hone analyze FILE --format json # JSON output
18
+ # hone analyze FILE --format github # GitHub Actions annotations
19
+ # hone analyze FILE --quiet # Minimal output
20
+ # hone analyze FILE --no-color # Disable colors
21
+ #
22
+ # @example Filtering
23
+ # hone analyze FILE --top=10 # Show only top 10 findings
24
+ # hone analyze FILE --hot-only # Only HOT findings
25
+ # hone analyze FILE --show-cold # Include COLD findings
26
+ #
27
+ # @example CI integration
28
+ # hone analyze FILE --fail-on hot # Exit 1 for HOT (default)
29
+ # hone analyze FILE --fail-on warm # Exit 2 for WARM, 1 for HOT
30
+ # hone analyze FILE --fail-on any # Exit 1 for any finding
31
+ # hone analyze FILE --fail-on none # Always exit 0
32
+ #
33
+ class CLI < Thor
34
+ def self.exit_on_failure?
35
+ true
36
+ end
37
+
38
+ desc "version", "Print version"
39
+ def version
40
+ puts "Hone v#{Hone::VERSION}"
41
+ end
42
+ map %w[-v --version] => :version
43
+
44
+ desc "analyze PATH", "Analyze Ruby file(s) for optimization opportunities"
45
+ long_desc <<~DESC
46
+ Analyze Ruby source files for performance optimization opportunities.
47
+
48
+ When run with --profile, findings are correlated with runtime profiling data
49
+ to prioritize optimizations that will have the greatest impact. Findings are
50
+ categorized as HOT (>5% CPU), WARM (1-5% CPU), or COLD (<1% CPU).
51
+
52
+ Without --profile, all findings are reported without prioritization.
53
+
54
+ Exit codes:
55
+ 0 - No findings, or only COLD findings
56
+ 1 - HOT findings exist
57
+ 2 - WARM findings exist (no HOT)
58
+ 3 - Error occurred
59
+
60
+ Examples:
61
+ hone analyze app.rb
62
+ hone analyze lib/ --profile profile.json
63
+ hone analyze . --format github --fail-on warm
64
+ DESC
65
+ option :profile,
66
+ type: :string,
67
+ desc: "StackProf JSON profile for correlation",
68
+ aliases: "-p"
69
+ option :memory_profile,
70
+ type: :string,
71
+ desc: "MemoryProfiler JSON for allocation correlation",
72
+ aliases: "-m"
73
+ option :format,
74
+ type: :string,
75
+ default: "text",
76
+ desc: "Output format: text, json, github, sarif, junit, tsv",
77
+ aliases: "-f"
78
+ option :color,
79
+ type: :boolean,
80
+ default: nil,
81
+ desc: "Force color output (default: auto-detect TTY)"
82
+ option :quiet,
83
+ type: :boolean,
84
+ default: false,
85
+ desc: "Minimal output (one line per finding)",
86
+ aliases: "-q"
87
+ option :verbose,
88
+ type: :boolean,
89
+ default: false,
90
+ desc: "Extended output with pattern details",
91
+ aliases: "-V"
92
+ option :top,
93
+ type: :numeric,
94
+ desc: "Show only top N findings"
95
+ option :hot_only,
96
+ type: :boolean,
97
+ default: false,
98
+ desc: "Only show HOT findings"
99
+ option :show_cold,
100
+ type: :boolean,
101
+ default: false,
102
+ desc: "Include COLD findings in output"
103
+ option :fail_on,
104
+ type: :string,
105
+ default: "hot",
106
+ desc: "Exit non-zero for: hot (default), warm, any, none"
107
+ option :diff,
108
+ type: :string,
109
+ desc: "Only analyze files changed since BASE ref"
110
+ option :baseline,
111
+ type: :string,
112
+ desc: "Suppress findings in baseline JSON file"
113
+ option :rails,
114
+ type: :boolean,
115
+ default: false,
116
+ desc: "Include Rails/ActiveSupport-specific optimizations"
117
+ def analyze(path)
118
+ analyzer = Analyzer.new(
119
+ path,
120
+ profile: options[:profile],
121
+ memory_profile: options[:memory_profile],
122
+ format: options[:format],
123
+ color: options[:color],
124
+ quiet: options[:quiet],
125
+ verbose: options[:verbose],
126
+ top: options[:top],
127
+ hot_only: options[:hot_only],
128
+ show_cold: options[:show_cold],
129
+ fail_on: options[:fail_on],
130
+ diff: options[:diff],
131
+ baseline: options[:baseline],
132
+ rails: options[:rails]
133
+ )
134
+
135
+ result = analyzer.run
136
+
137
+ puts result.output unless result.output.empty?
138
+
139
+ exit result.exit_code
140
+ end
141
+
142
+ desc "init COMPONENT", "Initialize Hone components"
143
+ long_desc <<~DESC
144
+ Initialize Hone components in your project.
145
+
146
+ Components:
147
+ harness - Generate a performance harness template
148
+
149
+ Examples:
150
+ hone init harness # Generate basic Ruby harness
151
+ hone init harness --rails # Generate Rails-specific harness
152
+ DESC
153
+ option :rails,
154
+ type: :boolean,
155
+ default: false,
156
+ desc: "Generate Rails-specific harness template"
157
+ def init(component)
158
+ case component
159
+ when "harness"
160
+ generator = HarnessGenerator.new(rails: options[:rails])
161
+ generator.generate
162
+ else
163
+ puts "Unknown component: #{component}"
164
+ puts "Available components: harness"
165
+ exit 1
166
+ end
167
+ end
168
+
169
+ desc "profile", "Run harness and generate performance profiles"
170
+ long_desc <<~DESC
171
+ Run the performance harness and generate CPU (and optionally memory) profiles.
172
+
173
+ The harness file should define setup, exercise, and teardown blocks.
174
+ Profiles are saved to tmp/hone/ by default.
175
+
176
+ Examples:
177
+ hone profile # Run default harness
178
+ hone profile --memory # Include memory profiling
179
+ hone profile --analyze # Profile then analyze
180
+ hone profile --harness custom.rb # Use custom harness file
181
+ DESC
182
+ option :harness,
183
+ type: :string,
184
+ default: ".hone/harness.rb",
185
+ desc: "Path to harness file"
186
+ option :profiler,
187
+ type: :string,
188
+ enum: %w[auto stackprof vernier],
189
+ default: "auto",
190
+ desc: "CPU profiler to use"
191
+ option :memory,
192
+ type: :boolean,
193
+ default: false,
194
+ desc: "Include memory profiling"
195
+ option :warmup,
196
+ type: :numeric,
197
+ default: 10,
198
+ desc: "Warmup iterations before profiling"
199
+ option :output,
200
+ type: :string,
201
+ default: "tmp/hone",
202
+ desc: "Output directory for profiles"
203
+ option :analyze,
204
+ type: :boolean,
205
+ default: false,
206
+ desc: "Run analysis after profiling"
207
+ def profile
208
+ unless File.exist?(options[:harness])
209
+ puts "Harness file not found: #{options[:harness]}"
210
+ puts "Run 'hone init harness' to generate a template."
211
+ exit 1
212
+ end
213
+
214
+ profiler_opt = (options[:profiler] == "auto") ? nil : options[:profiler].to_sym
215
+
216
+ runner = HarnessRunner.new(
217
+ options[:harness],
218
+ profiler: profiler_opt,
219
+ memory: options[:memory],
220
+ warmup: options[:warmup],
221
+ output_dir: options[:output]
222
+ )
223
+
224
+ puts "Loading harness from #{options[:harness]}..."
225
+ puts "Warmup: #{options[:warmup]} iterations"
226
+ puts "Profiler: #{profiler_opt || "auto-detect"}"
227
+ puts
228
+
229
+ profiles = runner.run
230
+
231
+ puts "Profiles generated:"
232
+ puts " CPU: #{profiles[:cpu]}"
233
+ puts " Memory: #{profiles[:memory]}" if profiles[:memory]
234
+ puts
235
+
236
+ if options[:analyze]
237
+ puts "Running analysis..."
238
+ invoke :analyze, ["."], profile: profiles[:cpu], memory_profile: profiles[:memory]
239
+ else
240
+ puts "Run analysis with:"
241
+ puts " hone analyze . --profile #{profiles[:cpu]}"
242
+ end
243
+ end
244
+
245
+ default_task :analyze
246
+ end
247
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Hone
6
+ class Config
7
+ CONFIG_FILE = ".hone/config.yml"
8
+
9
+ DEFAULTS = {
10
+ harness: {
11
+ warmup_iterations: 10,
12
+ profile_iterations: 100,
13
+ path: ".hone/harness.rb"
14
+ },
15
+ profilers: {
16
+ cpu: "auto",
17
+ memory: false
18
+ },
19
+ output: {
20
+ dir: "tmp/hone"
21
+ },
22
+ analysis: {
23
+ rails: false,
24
+ show_cold: false,
25
+ top: nil
26
+ }
27
+ }.freeze
28
+
29
+ def initialize(path = CONFIG_FILE)
30
+ @path = path
31
+ @data = load_config
32
+ end
33
+
34
+ def harness
35
+ @data[:harness]
36
+ end
37
+
38
+ def profilers
39
+ @data[:profilers]
40
+ end
41
+
42
+ def output
43
+ @data[:output]
44
+ end
45
+
46
+ def analysis
47
+ @data[:analysis]
48
+ end
49
+
50
+ def [](key)
51
+ @data[key.to_sym]
52
+ end
53
+
54
+ def exist?
55
+ File.exist?(@path)
56
+ end
57
+
58
+ private
59
+
60
+ def load_config
61
+ return deep_symbolize(DEFAULTS.dup) unless File.exist?(@path)
62
+
63
+ user_config = YAML.safe_load_file(@path, symbolize_names: true) || {}
64
+ deep_merge(DEFAULTS, user_config)
65
+ rescue Psych::SyntaxError => e
66
+ warn "Warning: Invalid config file #{@path}: #{e.message}"
67
+ deep_symbolize(DEFAULTS.dup)
68
+ end
69
+
70
+ def deep_merge(base, override)
71
+ result = {}
72
+ base.each do |key, value|
73
+ result[key] = if value.is_a?(Hash) && override[key].is_a?(Hash)
74
+ deep_merge(value, override[key])
75
+ elsif override.key?(key)
76
+ override[key]
77
+ else
78
+ value
79
+ end
80
+ end
81
+ override.each do |key, value|
82
+ result[key] = value unless result.key?(key)
83
+ end
84
+ result
85
+ end
86
+
87
+ def deep_symbolize(hash)
88
+ hash.transform_keys(&:to_sym).transform_values do |v|
89
+ v.is_a?(Hash) ? deep_symbolize(v) : v
90
+ end
91
+ end
92
+ end
93
+ end