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.
- checksums.yaml +7 -0
- data/.standard.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/Rakefile +10 -0
- data/examples/.hone/harness.rb +41 -0
- data/examples/README.md +22 -0
- data/examples/allocation_patterns.rb +66 -0
- data/examples/cpu_patterns.rb +50 -0
- data/examples/jit_patterns.rb +69 -0
- data/exe/hone +7 -0
- data/lib/hone/adapters/base.rb +35 -0
- data/lib/hone/adapters/fasterer.rb +38 -0
- data/lib/hone/adapters/rubocop_performance.rb +85 -0
- data/lib/hone/analyzer.rb +258 -0
- data/lib/hone/cli.rb +247 -0
- data/lib/hone/config.rb +93 -0
- data/lib/hone/correlator.rb +250 -0
- data/lib/hone/exit_codes.rb +10 -0
- data/lib/hone/finding.rb +64 -0
- data/lib/hone/finding_filter.rb +57 -0
- data/lib/hone/formatters/base.rb +25 -0
- data/lib/hone/formatters/filterable.rb +31 -0
- data/lib/hone/formatters/github.rb +71 -0
- data/lib/hone/formatters/json.rb +75 -0
- data/lib/hone/formatters/junit.rb +154 -0
- data/lib/hone/formatters/sarif.rb +179 -0
- data/lib/hone/formatters/tsv.rb +49 -0
- data/lib/hone/harness.rb +57 -0
- data/lib/hone/harness_generator.rb +128 -0
- data/lib/hone/harness_runner.rb +172 -0
- data/lib/hone/method_map.rb +140 -0
- data/lib/hone/patterns/README.md +174 -0
- data/lib/hone/patterns/array_compact.rb +105 -0
- data/lib/hone/patterns/array_include_set.rb +34 -0
- data/lib/hone/patterns/base.rb +90 -0
- data/lib/hone/patterns/block_to_proc.rb +109 -0
- data/lib/hone/patterns/bsearch_vs_find.rb +80 -0
- data/lib/hone/patterns/chars_map_ord.rb +42 -0
- data/lib/hone/patterns/chars_to_variable.rb +136 -0
- data/lib/hone/patterns/chars_to_variable_tainted.rb +136 -0
- data/lib/hone/patterns/constant_regexp.rb +74 -0
- data/lib/hone/patterns/count_vs_size.rb +35 -0
- data/lib/hone/patterns/divmod.rb +92 -0
- data/lib/hone/patterns/dynamic_ivar.rb +44 -0
- data/lib/hone/patterns/dynamic_ivar_get.rb +33 -0
- data/lib/hone/patterns/each_with_index.rb +116 -0
- data/lib/hone/patterns/each_with_object.rb +63 -0
- data/lib/hone/patterns/flatten_once.rb +28 -0
- data/lib/hone/patterns/gsub_to_tr.rb +48 -0
- data/lib/hone/patterns/hash_each_key.rb +41 -0
- data/lib/hone/patterns/hash_each_value.rb +31 -0
- data/lib/hone/patterns/hash_keys_include.rb +30 -0
- data/lib/hone/patterns/hash_merge_bang.rb +33 -0
- data/lib/hone/patterns/hash_values_include.rb +31 -0
- data/lib/hone/patterns/inject_sum.rb +48 -0
- data/lib/hone/patterns/kernel_loop.rb +27 -0
- data/lib/hone/patterns/lazy_ivar.rb +39 -0
- data/lib/hone/patterns/map_compact.rb +32 -0
- data/lib/hone/patterns/map_flatten.rb +31 -0
- data/lib/hone/patterns/map_select_chain.rb +32 -0
- data/lib/hone/patterns/parallel_assignment.rb +127 -0
- data/lib/hone/patterns/positive_predicate.rb +27 -0
- data/lib/hone/patterns/range_include.rb +34 -0
- data/lib/hone/patterns/redundant_string_chars.rb +82 -0
- data/lib/hone/patterns/regexp_match.rb +126 -0
- data/lib/hone/patterns/reverse_each.rb +30 -0
- data/lib/hone/patterns/reverse_first.rb +40 -0
- data/lib/hone/patterns/select_count.rb +32 -0
- data/lib/hone/patterns/select_first.rb +31 -0
- data/lib/hone/patterns/select_map.rb +32 -0
- data/lib/hone/patterns/shuffle_first.rb +30 -0
- data/lib/hone/patterns/slice_with_length.rb +48 -0
- data/lib/hone/patterns/sort_by_first.rb +31 -0
- data/lib/hone/patterns/sort_by_last.rb +31 -0
- data/lib/hone/patterns/sort_first.rb +52 -0
- data/lib/hone/patterns/sort_last.rb +30 -0
- data/lib/hone/patterns/sort_reverse.rb +53 -0
- data/lib/hone/patterns/string_casecmp.rb +54 -0
- data/lib/hone/patterns/string_chars_each.rb +56 -0
- data/lib/hone/patterns/string_concat_in_loop.rb +116 -0
- data/lib/hone/patterns/string_delete_prefix.rb +53 -0
- data/lib/hone/patterns/string_delete_suffix.rb +53 -0
- data/lib/hone/patterns/string_empty.rb +64 -0
- data/lib/hone/patterns/string_end_with.rb +81 -0
- data/lib/hone/patterns/string_shovel.rb +75 -0
- data/lib/hone/patterns/string_start_with.rb +80 -0
- data/lib/hone/patterns/taint_tracking_base.rb +230 -0
- data/lib/hone/patterns/times_map.rb +38 -0
- data/lib/hone/patterns/uniq_by.rb +32 -0
- data/lib/hone/patterns/yield_vs_block.rb +72 -0
- data/lib/hone/profilers/base.rb +162 -0
- data/lib/hone/profilers/factory.rb +31 -0
- data/lib/hone/profilers/memory_profiler.rb +213 -0
- data/lib/hone/profilers/stackprof.rb +99 -0
- data/lib/hone/profilers/vernier.rb +147 -0
- data/lib/hone/reporter.rb +371 -0
- data/lib/hone/scanner.rb +75 -0
- data/lib/hone/suggestion_generator.rb +23 -0
- data/lib/hone/version.rb +5 -0
- data/lib/hone.rb +108 -0
- data/logo.png +0 -0
- data/sig/hone.rbs +4 -0
- metadata +176 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Formatters
|
|
5
|
+
class JUnit < Base
|
|
6
|
+
PRIORITY_TO_TYPE = {
|
|
7
|
+
hot_cpu: "failure",
|
|
8
|
+
hot_alloc: "failure",
|
|
9
|
+
jit_unfriendly: "failure",
|
|
10
|
+
warm: "failure",
|
|
11
|
+
cold: "skipped",
|
|
12
|
+
unknown: "failure"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def format
|
|
16
|
+
require_rexml!
|
|
17
|
+
doc = build_document
|
|
18
|
+
output = +""
|
|
19
|
+
formatter = REXML::Formatters::Pretty.new(2)
|
|
20
|
+
formatter.compact = true
|
|
21
|
+
output << %(<?xml version="1.0" encoding="UTF-8"?>\n)
|
|
22
|
+
formatter.write(doc.root, output)
|
|
23
|
+
output
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def require_rexml!
|
|
29
|
+
require "rexml/document"
|
|
30
|
+
rescue LoadError
|
|
31
|
+
raise Hone::Error, "JUnit format requires the 'rexml' gem. Add it to your Gemfile: gem 'rexml'"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_document
|
|
35
|
+
doc = REXML::Document.new
|
|
36
|
+
testsuites = doc.add_element("testsuites")
|
|
37
|
+
testsuites.add_attribute("name", "Hone")
|
|
38
|
+
|
|
39
|
+
findings_by_file = filtered_findings.group_by(&:file)
|
|
40
|
+
|
|
41
|
+
total_tests = 0
|
|
42
|
+
total_failures = 0
|
|
43
|
+
total_skipped = 0
|
|
44
|
+
|
|
45
|
+
findings_by_file.each do |file, file_findings|
|
|
46
|
+
suite = testsuites.add_element("testsuite")
|
|
47
|
+
suite.add_attribute("name", file)
|
|
48
|
+
|
|
49
|
+
suite_failures = 0
|
|
50
|
+
suite_skipped = 0
|
|
51
|
+
|
|
52
|
+
file_findings.each do |finding|
|
|
53
|
+
add_testcase(suite, finding, file)
|
|
54
|
+
|
|
55
|
+
case result_type(finding)
|
|
56
|
+
when "failure"
|
|
57
|
+
suite_failures += 1
|
|
58
|
+
when "skipped"
|
|
59
|
+
suite_skipped += 1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
suite.add_attribute("tests", file_findings.size.to_s)
|
|
64
|
+
suite.add_attribute("failures", suite_failures.to_s)
|
|
65
|
+
suite.add_attribute("skipped", suite_skipped.to_s)
|
|
66
|
+
|
|
67
|
+
total_tests += file_findings.size
|
|
68
|
+
total_failures += suite_failures
|
|
69
|
+
total_skipped += suite_skipped
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
testsuites.add_attribute("tests", total_tests.to_s)
|
|
73
|
+
testsuites.add_attribute("failures", total_failures.to_s)
|
|
74
|
+
testsuites.add_attribute("skipped", total_skipped.to_s)
|
|
75
|
+
|
|
76
|
+
doc
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_testcase(suite, finding, file)
|
|
80
|
+
testcase = suite.add_element("testcase")
|
|
81
|
+
testcase.add_attribute("name", testcase_name(finding))
|
|
82
|
+
testcase.add_attribute("classname", classname_from_file(file))
|
|
83
|
+
|
|
84
|
+
case result_type(finding)
|
|
85
|
+
when "failure"
|
|
86
|
+
add_failure_element(testcase, finding)
|
|
87
|
+
when "skipped"
|
|
88
|
+
add_skipped_element(testcase, finding)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def testcase_name(finding)
|
|
93
|
+
name = finding.pattern_id.to_s
|
|
94
|
+
name += " at line #{finding.line}" if finding.line
|
|
95
|
+
name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def classname_from_file(file)
|
|
99
|
+
# Convert path like "app/models/order.rb" to "app.models.order"
|
|
100
|
+
file
|
|
101
|
+
.sub(/\.rb\z/, "")
|
|
102
|
+
.tr("/", ".")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def result_type(finding)
|
|
106
|
+
priority = finding.priority || :unknown
|
|
107
|
+
PRIORITY_TO_TYPE.fetch(priority, "failure")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def add_failure_element(testcase, finding)
|
|
111
|
+
failure = testcase.add_element("failure")
|
|
112
|
+
failure.add_attribute("message", finding.message)
|
|
113
|
+
failure.add_attribute("type", (finding.priority || :unknown).to_s)
|
|
114
|
+
failure.add_text(failure_details(finding))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def add_skipped_element(testcase, finding)
|
|
118
|
+
skipped = testcase.add_element("skipped")
|
|
119
|
+
skipped.add_attribute("message", finding.message)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def failure_details(finding)
|
|
123
|
+
heat = priority_label(finding.priority || :unknown)
|
|
124
|
+
|
|
125
|
+
cpu_info = finding.cpu_percent ? "#{format_percent(finding.cpu_percent)} CPU" : nil
|
|
126
|
+
alloc_info = finding.alloc_percent ? "#{format_percent(finding.alloc_percent)} alloc" : nil
|
|
127
|
+
metrics = [cpu_info, alloc_info].compact.join(", ")
|
|
128
|
+
|
|
129
|
+
details = if metrics.empty?
|
|
130
|
+
"[#{heat}] #{finding.message}"
|
|
131
|
+
else
|
|
132
|
+
"[#{heat} #{metrics}] #{finding.message}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
details += "\nCode: #{finding.code}" if finding.code
|
|
136
|
+
details += "\nFile: #{finding.file}:#{finding.line}" if finding.file && finding.line
|
|
137
|
+
details
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def priority_label(priority)
|
|
141
|
+
case priority
|
|
142
|
+
when :hot_cpu then "HOT-CPU"
|
|
143
|
+
when :hot_alloc then "HOT-ALLOC"
|
|
144
|
+
when :jit_unfriendly then "JIT-UNFRIENDLY"
|
|
145
|
+
else priority.to_s.upcase
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def format_percent(percent)
|
|
150
|
+
"%.1f%%" % percent
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hone
|
|
6
|
+
module Formatters
|
|
7
|
+
# SARIF (Static Analysis Results Interchange Format) formatter for GitHub Code Scanning.
|
|
8
|
+
#
|
|
9
|
+
# SARIF is a standard format for static analysis tools, supported by GitHub Code Scanning
|
|
10
|
+
# and other security/code quality platforms.
|
|
11
|
+
#
|
|
12
|
+
# @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
|
|
13
|
+
#
|
|
14
|
+
# @example Usage
|
|
15
|
+
# formatter = Hone::Formatters::SARIF.new(findings)
|
|
16
|
+
# puts formatter.format
|
|
17
|
+
class SARIF < Base
|
|
18
|
+
SARIF_VERSION = "2.1.0"
|
|
19
|
+
SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
|
|
20
|
+
|
|
21
|
+
# Maps Hone priority levels to SARIF severity levels.
|
|
22
|
+
# - error: A serious problem that should be addressed immediately
|
|
23
|
+
# - warning: A potential problem that should be reviewed
|
|
24
|
+
# - note: Informational finding
|
|
25
|
+
PRIORITY_TO_LEVEL = {
|
|
26
|
+
hot_cpu: "error",
|
|
27
|
+
hot_alloc: "error",
|
|
28
|
+
jit_unfriendly: "warning",
|
|
29
|
+
warm: "warning",
|
|
30
|
+
cold: "note",
|
|
31
|
+
unknown: "note"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
def format
|
|
35
|
+
::JSON.pretty_generate(build_sarif)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_sarif
|
|
41
|
+
{
|
|
42
|
+
"$schema" => SARIF_SCHEMA,
|
|
43
|
+
"version" => SARIF_VERSION,
|
|
44
|
+
"runs" => [build_run]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_run
|
|
49
|
+
{
|
|
50
|
+
"tool" => build_tool,
|
|
51
|
+
"results" => build_results
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_tool
|
|
56
|
+
{
|
|
57
|
+
"driver" => {
|
|
58
|
+
"name" => "Hone",
|
|
59
|
+
"version" => Hone::VERSION,
|
|
60
|
+
"informationUri" => "https://github.com/your-org/hone",
|
|
61
|
+
"rules" => build_rules
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_rules
|
|
67
|
+
# Collect unique pattern IDs from findings to build rule definitions
|
|
68
|
+
unique_patterns = @findings.map(&:pattern_id).uniq
|
|
69
|
+
|
|
70
|
+
unique_patterns.map { |pattern_id| build_rule(pattern_id) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_rule(pattern_id)
|
|
74
|
+
{
|
|
75
|
+
"id" => pattern_id.to_s,
|
|
76
|
+
"name" => humanize_pattern_id(pattern_id),
|
|
77
|
+
"shortDescription" => {
|
|
78
|
+
"text" => "#{humanize_pattern_id(pattern_id)} optimization opportunity"
|
|
79
|
+
},
|
|
80
|
+
"helpUri" => "https://github.com/your-org/hone##{pattern_id}"
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def humanize_pattern_id(pattern_id)
|
|
85
|
+
pattern_id.to_s.split("_").map(&:capitalize).join(" ")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_results
|
|
89
|
+
@findings.map { |finding| build_result(finding) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_result(finding)
|
|
93
|
+
result = {
|
|
94
|
+
"ruleId" => finding.pattern_id.to_s,
|
|
95
|
+
"level" => sarif_level(finding),
|
|
96
|
+
"message" => {
|
|
97
|
+
"text" => build_message_text(finding)
|
|
98
|
+
},
|
|
99
|
+
"locations" => [build_location(finding)]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Add optional properties if available
|
|
103
|
+
result["partialFingerprints"] = build_fingerprints(finding) if finding.method_name
|
|
104
|
+
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def sarif_level(finding)
|
|
109
|
+
priority = finding.priority || :unknown
|
|
110
|
+
PRIORITY_TO_LEVEL.fetch(priority, "note")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_message_text(finding)
|
|
114
|
+
parts = [finding.message]
|
|
115
|
+
|
|
116
|
+
if finding.cpu_percent
|
|
117
|
+
parts << "CPU: %.1f%%" % finding.cpu_percent
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if finding.alloc_percent
|
|
121
|
+
parts << "Allocations: %.1f%%" % finding.alloc_percent
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if finding.speedup
|
|
125
|
+
parts << "Expected speedup: #{finding.speedup}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
parts.join(" | ")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_location(finding)
|
|
132
|
+
location = {
|
|
133
|
+
"physicalLocation" => {
|
|
134
|
+
"artifactLocation" => {
|
|
135
|
+
"uri" => finding.file
|
|
136
|
+
},
|
|
137
|
+
"region" => build_region(finding)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Add logical location (method name) if available
|
|
142
|
+
if finding.method_name
|
|
143
|
+
location["logicalLocations"] = [
|
|
144
|
+
{
|
|
145
|
+
"name" => finding.method_name,
|
|
146
|
+
"kind" => "function"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
location
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_region(finding)
|
|
155
|
+
region = {
|
|
156
|
+
"startLine" => finding.line
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
region["startColumn"] = finding.column if finding.column
|
|
160
|
+
|
|
161
|
+
# Include the source code snippet if available
|
|
162
|
+
if finding.code
|
|
163
|
+
region["snippet"] = {
|
|
164
|
+
"text" => finding.code
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
region
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def build_fingerprints(finding)
|
|
172
|
+
# Partial fingerprints help GitHub track results across runs
|
|
173
|
+
{
|
|
174
|
+
"methodName" => finding.method_name
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Formatters
|
|
5
|
+
class TSV < Base
|
|
6
|
+
HEADERS = %w[priority cpu_percent alloc_percent file line pattern_id message method_name].freeze
|
|
7
|
+
COLUMN_SEPARATOR = "\t"
|
|
8
|
+
|
|
9
|
+
def format
|
|
10
|
+
lines = [header_row]
|
|
11
|
+
lines.concat(filtered_findings.map { |finding| format_row(finding) })
|
|
12
|
+
lines.join("\n")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def header_row
|
|
18
|
+
HEADERS.join(COLUMN_SEPARATOR)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def format_row(finding)
|
|
22
|
+
[
|
|
23
|
+
(finding.priority || "").to_s,
|
|
24
|
+
format_percent(finding.cpu_percent),
|
|
25
|
+
format_percent(finding.alloc_percent),
|
|
26
|
+
finding.file.to_s,
|
|
27
|
+
finding.line.to_s,
|
|
28
|
+
finding.pattern_id.to_s,
|
|
29
|
+
escape_field(finding.message.to_s),
|
|
30
|
+
finding.method_name.to_s
|
|
31
|
+
].join(COLUMN_SEPARATOR)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def format_percent(value)
|
|
35
|
+
return "" if value.nil?
|
|
36
|
+
|
|
37
|
+
value.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def escape_field(value)
|
|
41
|
+
# Escape tabs and newlines for TSV compatibility
|
|
42
|
+
value
|
|
43
|
+
.gsub("\t", "\\t")
|
|
44
|
+
.gsub("\n", "\\n")
|
|
45
|
+
.gsub("\r", "\\r")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/hone/harness.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
class Harness
|
|
5
|
+
attr_reader :setup_block, :exercise_block, :teardown_block, :iterations
|
|
6
|
+
|
|
7
|
+
def self.load(path)
|
|
8
|
+
raise Hone::Error, "Harness file not found: #{path}" unless File.exist?(path)
|
|
9
|
+
|
|
10
|
+
harness = new
|
|
11
|
+
harness.instance_eval(File.read(path), path)
|
|
12
|
+
harness
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@setup_block = nil
|
|
17
|
+
@exercise_block = nil
|
|
18
|
+
@teardown_block = nil
|
|
19
|
+
@iterations = 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Setup block - runs once before profiling (not profiled)
|
|
23
|
+
# Used for loading the application, creating test data, etc.
|
|
24
|
+
def setup(&block)
|
|
25
|
+
@setup_block = block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Exercise block - the code to profile
|
|
29
|
+
# @param iterations [Integer] Number of times to run the block during profiling
|
|
30
|
+
def exercise(iterations: 1, &block)
|
|
31
|
+
@iterations = iterations
|
|
32
|
+
@exercise_block = block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Teardown block - runs once after profiling (not profiled)
|
|
36
|
+
# Used for cleanup, closing connections, etc.
|
|
37
|
+
def teardown(&block)
|
|
38
|
+
@teardown_block = block
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run_setup
|
|
42
|
+
@setup_block&.call
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run_exercise
|
|
46
|
+
@exercise_block&.call
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_teardown
|
|
50
|
+
@teardown_block&.call
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def valid?
|
|
54
|
+
!@exercise_block.nil?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Hone
|
|
6
|
+
class HarnessGenerator
|
|
7
|
+
HARNESS_DIR = ".hone"
|
|
8
|
+
HARNESS_FILE = "harness.rb"
|
|
9
|
+
|
|
10
|
+
def initialize(rails: false)
|
|
11
|
+
@rails = rails
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
FileUtils.mkdir_p(HARNESS_DIR)
|
|
16
|
+
|
|
17
|
+
path = File.join(HARNESS_DIR, HARNESS_FILE)
|
|
18
|
+
|
|
19
|
+
if File.exist?(path)
|
|
20
|
+
puts "Harness already exists: #{path}"
|
|
21
|
+
puts "Delete it first if you want to regenerate."
|
|
22
|
+
return false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
template = @rails ? rails_template : ruby_template
|
|
26
|
+
File.write(path, template)
|
|
27
|
+
|
|
28
|
+
puts "Created #{path}"
|
|
29
|
+
puts
|
|
30
|
+
puts "Next steps:"
|
|
31
|
+
puts " 1. Edit #{path} to exercise your application's hot paths"
|
|
32
|
+
puts " 2. Run: hone profile"
|
|
33
|
+
puts " 3. Run: hone analyze ."
|
|
34
|
+
puts
|
|
35
|
+
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def ruby_template
|
|
42
|
+
<<~RUBY
|
|
43
|
+
# frozen_string_literal: true
|
|
44
|
+
|
|
45
|
+
# Hone Performance Harness
|
|
46
|
+
# ========================
|
|
47
|
+
# This file defines how to exercise your code for profiling.
|
|
48
|
+
#
|
|
49
|
+
# Run with: hone profile
|
|
50
|
+
# Analyze: hone analyze . (uses generated profiles automatically)
|
|
51
|
+
|
|
52
|
+
# Setup: Load your application (not profiled)
|
|
53
|
+
setup do
|
|
54
|
+
# Load your library or application
|
|
55
|
+
# require_relative "../lib/my_gem"
|
|
56
|
+
|
|
57
|
+
# Create any test data needed
|
|
58
|
+
# @data = generate_test_data
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Exercise: The code to profile
|
|
62
|
+
# This block runs multiple times during profiling.
|
|
63
|
+
# Put your realistic workload here.
|
|
64
|
+
exercise iterations: 100 do
|
|
65
|
+
# Example: Call your hot methods
|
|
66
|
+
# result = MyClass.new(@data).process
|
|
67
|
+
#
|
|
68
|
+
# Example: Simulate typical usage
|
|
69
|
+
# parser = Parser.new
|
|
70
|
+
# 100.times { parser.parse(sample_input) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Teardown: Cleanup (not profiled)
|
|
74
|
+
teardown do
|
|
75
|
+
# Close connections, clean up temp files, etc.
|
|
76
|
+
end
|
|
77
|
+
RUBY
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rails_template
|
|
81
|
+
<<~RUBY
|
|
82
|
+
# frozen_string_literal: true
|
|
83
|
+
|
|
84
|
+
# Hone Performance Harness (Rails)
|
|
85
|
+
# =================================
|
|
86
|
+
# This file defines how to exercise your Rails app for profiling.
|
|
87
|
+
#
|
|
88
|
+
# Run with: hone profile
|
|
89
|
+
# Analyze: hone analyze app/ (uses generated profiles automatically)
|
|
90
|
+
|
|
91
|
+
# Setup: Boot Rails (not profiled)
|
|
92
|
+
setup do
|
|
93
|
+
require_relative "../config/environment"
|
|
94
|
+
Rails.application.eager_load!
|
|
95
|
+
|
|
96
|
+
# Create test data if needed
|
|
97
|
+
# @user = User.first || User.create!(name: "Test", email: "test@example.com")
|
|
98
|
+
# @items = Item.limit(10).to_a
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Exercise: Your hot paths
|
|
102
|
+
# This block runs multiple times during profiling.
|
|
103
|
+
# Replace with YOUR app's actual hot code paths.
|
|
104
|
+
exercise iterations: 100 do
|
|
105
|
+
# Example: Model queries
|
|
106
|
+
# User.where(active: true).includes(:orders).limit(10).to_a
|
|
107
|
+
|
|
108
|
+
# Example: Business logic
|
|
109
|
+
# Order.new(user: @user, items: @items).calculate_total
|
|
110
|
+
|
|
111
|
+
# Example: Service objects
|
|
112
|
+
# PaymentProcessor.new(order).validate
|
|
113
|
+
|
|
114
|
+
# Example: Simulate a controller action
|
|
115
|
+
# app = Rails.application
|
|
116
|
+
# env = Rack::MockRequest.env_for("/users/1")
|
|
117
|
+
# app.call(env)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Teardown: Cleanup (not profiled)
|
|
121
|
+
teardown do
|
|
122
|
+
# Clean up test data if needed
|
|
123
|
+
# @user&.destroy
|
|
124
|
+
end
|
|
125
|
+
RUBY
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|