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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c35d339dc059e423c261e29acd55d76091c09e94cbd1b945eb5c6436b79909ad
|
|
4
|
+
data.tar.gz: 48ed4d9d8ef4a6484e55fb4fd9d71a0cba5285aee2bc52c6436644c631cec33c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f0cd2110c5a28b7e35cb463e00a118cd63e0a9ef2ca206e4d7bb38afc76223b8c1f319a857f4e9cb788d4a29293694312f874efac0580774d0a0b922afb8e654
|
|
7
|
+
data.tar.gz: 8c2058eed3b4699e9ada63edaa316b2f3029aeb9728568bbb614ba8d521986af7b612be6eb75fc49eda7f6f3c55cc33f7ce94355764a6b73516464c2cf59eb03
|
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stephen Ierodiaconou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.png" alt="Hone" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Hone
|
|
6
|
+
|
|
7
|
+
> **Note:** This is currently a proof of concept. APIs and output formats may change.
|
|
8
|
+
|
|
9
|
+
Find Ruby performance optimizations that actually matter by combining static analysis with runtime profiling.
|
|
10
|
+
|
|
11
|
+
## Example Output
|
|
12
|
+
|
|
13
|
+
When run with profiling data, performance optimisation findings show their potential runtime impact:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Hone v0.1.0
|
|
17
|
+
|
|
18
|
+
Analyzing lib/sqids.rb with profile: tmp/hone/cpu_profile.json
|
|
19
|
+
────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
lib/sqids.rb:69 in `Sqids#decode`
|
|
22
|
+
17.5% of allocations — High impact
|
|
23
|
+
|
|
24
|
+
67 │
|
|
25
|
+
68 │ alphabet_chars = @alphabet.chars
|
|
26
|
+
69 │ id.chars.each do |c|
|
|
27
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
28
|
+
70 │ return ret unless alphabet_chars.include?(c)
|
|
29
|
+
71 │ end
|
|
30
|
+
|
|
31
|
+
Use `id.each_char { ... }` instead of `chars.each` to avoid intermediate array
|
|
32
|
+
|
|
33
|
+
────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
lib/sqids.rb:183 in `Sqids#blocked_id?`
|
|
36
|
+
13.5% of CPU — High impact
|
|
37
|
+
|
|
38
|
+
181 │ id.start_with?(word) || id.end_with?(word)
|
|
39
|
+
182 │ else
|
|
40
|
+
183 │ id.include?(word)
|
|
41
|
+
^^^^^^^^^^^^^^^^^
|
|
42
|
+
184 │ end
|
|
43
|
+
185 │ end
|
|
44
|
+
|
|
45
|
+
Consider using Set instead of Array#include? for repeated lookups
|
|
46
|
+
|
|
47
|
+
────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
lib/sqids.rb:104 in `Sqids#shuffle`
|
|
50
|
+
3.1% of CPU — Moderate
|
|
51
|
+
|
|
52
|
+
102 │ i = 0
|
|
53
|
+
103 │ j = chars.length - 1
|
|
54
|
+
104 │ while j.positive?
|
|
55
|
+
^^^^^^^^^^^
|
|
56
|
+
105 │ r = ((i * j) + chars[i].ord + chars[j].ord) % chars.length
|
|
57
|
+
106 │ chars[i], chars[r] = chars[r], chars[i]
|
|
58
|
+
|
|
59
|
+
Fix: j > 0
|
|
60
|
+
Use `> 0` instead of `.positive?` to avoid method call overhead
|
|
61
|
+
|
|
62
|
+
────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
Summary: 14 findings
|
|
65
|
+
|
|
66
|
+
3 high impact (>5% of CPU or allocations) ← fix these first
|
|
67
|
+
11 moderate (1-5%)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The percentages show how much CPU time or memory allocation each method used during profiling, helping you focus on fixes that matter.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
Add to your Gemfile:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
gem 'hone', group: :development
|
|
78
|
+
|
|
79
|
+
# Optional: for profiling integration
|
|
80
|
+
gem 'stackprof', group: :development
|
|
81
|
+
gem 'memory_profiler', group: :development
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or install directly:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
gem install hone
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Quick Start
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Try it on the included examples
|
|
94
|
+
hone analyze examples/
|
|
95
|
+
|
|
96
|
+
# Or analyze your own code
|
|
97
|
+
hone analyze lib/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### With Profiling (Recommended)
|
|
101
|
+
|
|
102
|
+
For prioritized results, use a harness:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Create a harness in your project
|
|
106
|
+
hone init harness
|
|
107
|
+
|
|
108
|
+
# Edit .hone/harness.rb to define your workload, then:
|
|
109
|
+
hone profile --analyze
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or provide an existing StackProf profile:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
hone analyze lib/ --profile stackprof.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### About Impact Levels
|
|
119
|
+
|
|
120
|
+
| Level | Meaning |
|
|
121
|
+
|-------|---------|
|
|
122
|
+
| High impact | >5% CPU or allocation - fix these first |
|
|
123
|
+
| JIT issue | Hurts YJIT optimization |
|
|
124
|
+
| Moderate | 1-5% impact - worth fixing |
|
|
125
|
+
| Low | <1% impact - low priority |
|
|
126
|
+
|
|
127
|
+
Without profiling data, findings show "Unknown" impact.
|
|
128
|
+
|
|
129
|
+
## CLI Reference
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Analysis
|
|
133
|
+
hone analyze FILE|DIR # Static analysis
|
|
134
|
+
hone analyze FILE --profile STACKPROF # With CPU correlation
|
|
135
|
+
hone analyze FILE --memory-profile FILE # With memory correlation
|
|
136
|
+
|
|
137
|
+
# Profiling
|
|
138
|
+
hone init harness # Create .hone/harness.rb template
|
|
139
|
+
hone profile # Run harness, generate profiles
|
|
140
|
+
hone profile --analyze # Profile and analyze in one step
|
|
141
|
+
hone profile --memory # Include memory profiling
|
|
142
|
+
|
|
143
|
+
# Output control
|
|
144
|
+
--format FORMAT # text, json, github, sarif, junit, tsv
|
|
145
|
+
--top N # Show only top N findings
|
|
146
|
+
--hot-only # Only high impact findings
|
|
147
|
+
--show-cold # Include low impact findings (hidden by default)
|
|
148
|
+
--quiet / -q # One line per finding
|
|
149
|
+
--verbose / -V # Extended output with pattern details
|
|
150
|
+
|
|
151
|
+
# CI integration
|
|
152
|
+
--fail-on LEVEL # Exit non-zero for: hot, warm, any, none
|
|
153
|
+
--baseline FILE # Suppress known findings
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Harness
|
|
157
|
+
|
|
158
|
+
The harness defines a repeatable workload for profiling:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# .hone/harness.rb
|
|
162
|
+
setup do
|
|
163
|
+
require_relative '../lib/my_gem'
|
|
164
|
+
@data = load_test_data
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
exercise iterations: 100 do
|
|
168
|
+
MyGem.process(@data)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
teardown do
|
|
172
|
+
cleanup # optional
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `setup`: Runs once before profiling (load code, prepare data)
|
|
177
|
+
- `exercise`: The code to profile (runs N iterations)
|
|
178
|
+
- `teardown`: Cleanup after profiling (optional)
|
|
179
|
+
|
|
180
|
+
## Patterns
|
|
181
|
+
|
|
182
|
+
Hone detects 50+ patterns across three categories:
|
|
183
|
+
|
|
184
|
+
- **CPU**: Method call overhead, loop inefficiencies
|
|
185
|
+
- **Allocation**: Intermediate arrays, string allocations
|
|
186
|
+
- **JIT**: Dynamic ivars, shape transitions, YJIT blockers
|
|
187
|
+
|
|
188
|
+
Run `hone patterns` to list all available patterns.
|
|
189
|
+
|
|
190
|
+
## Requirements
|
|
191
|
+
|
|
192
|
+
- Ruby 3.1+
|
|
193
|
+
- Prism (bundled with Ruby 3.3+, add `gem 'prism'` for earlier versions)
|
|
194
|
+
|
|
195
|
+
## Acknowledgements
|
|
196
|
+
|
|
197
|
+
Some pattern detections inspired by:
|
|
198
|
+
|
|
199
|
+
- [fast-ruby](https://github.com/fastruby/fast-ruby) - Benchmarks for common Ruby idioms
|
|
200
|
+
- [fasterer](https://github.com/DamirSvrtan/fasterer) - Static analysis for speed improvements
|
|
201
|
+
- [rubocop-performance](https://github.com/rubocop/rubocop-performance) - Performance-focused RuboCop cops
|
data/Rakefile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Hone harness for example patterns
|
|
2
|
+
# Run with: hone profile --analyze
|
|
3
|
+
|
|
4
|
+
setup do
|
|
5
|
+
require_relative "../cpu_patterns"
|
|
6
|
+
require_relative "../allocation_patterns"
|
|
7
|
+
require_relative "../jit_patterns"
|
|
8
|
+
|
|
9
|
+
@cpu = CpuPatterns.new
|
|
10
|
+
@alloc = AllocationPatterns.new
|
|
11
|
+
@jit = JitPatterns.new
|
|
12
|
+
@data = (1..100).to_a
|
|
13
|
+
@str = "hello world" * 10
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
exercise iterations: 1000 do
|
|
17
|
+
# CPU patterns
|
|
18
|
+
@cpu.check_positive(42)
|
|
19
|
+
@cpu.count_up
|
|
20
|
+
@cpu.sum_with_index(@data)
|
|
21
|
+
@cpu.tail(@str)
|
|
22
|
+
@cpu.replace_spaces(@str)
|
|
23
|
+
@cpu.match_pattern(["abc", "123", "def"])
|
|
24
|
+
|
|
25
|
+
# Allocation patterns
|
|
26
|
+
@alloc.char_codes(@str)
|
|
27
|
+
@alloc.transform_and_filter(@data)
|
|
28
|
+
@alloc.iterate_chars("test")
|
|
29
|
+
@alloc.nested_map([1, 2, 3])
|
|
30
|
+
@alloc.total(@data)
|
|
31
|
+
@alloc.random_element(@data)
|
|
32
|
+
@alloc.smallest(@data)
|
|
33
|
+
@alloc.final_element(@data)
|
|
34
|
+
@alloc.build_string(%w[a b c d e])
|
|
35
|
+
@alloc.char_at(@str, 5)
|
|
36
|
+
|
|
37
|
+
# JIT patterns
|
|
38
|
+
@jit.set_field("test", 123)
|
|
39
|
+
@jit.get_field("test")
|
|
40
|
+
@jit.cached_value
|
|
41
|
+
end
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Hone Examples
|
|
2
|
+
|
|
3
|
+
Example files with intentional anti-patterns for testing Hone.
|
|
4
|
+
|
|
5
|
+
## Quick Test
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd examples
|
|
9
|
+
|
|
10
|
+
# Static analysis only
|
|
11
|
+
hone analyze .
|
|
12
|
+
|
|
13
|
+
# With profiling (recommended)
|
|
14
|
+
hone profile --memory --analyze
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Files
|
|
18
|
+
|
|
19
|
+
- `cpu_patterns.rb` - Method call overhead, loop inefficiencies
|
|
20
|
+
- `allocation_patterns.rb` - Intermediate arrays, string allocations
|
|
21
|
+
- `jit_patterns.rb` - YJIT optimization blockers
|
|
22
|
+
- `.hone/harness.rb` - Profiling harness for these examples
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable all
|
|
4
|
+
# standardrb:disable all
|
|
5
|
+
|
|
6
|
+
# Allocation-related patterns that Hone detects
|
|
7
|
+
# NOTE: This file intentionally contains anti-patterns for testing
|
|
8
|
+
|
|
9
|
+
class AllocationPatterns
|
|
10
|
+
# chars.map(&:ord) vs bytes
|
|
11
|
+
def char_codes(str)
|
|
12
|
+
str.chars.map(&:ord) # Hone: Use `str.bytes` instead
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# map.select vs filter_map
|
|
16
|
+
def transform_and_filter(arr)
|
|
17
|
+
arr.map { |x| x * 2 }.select { |x| x > 10 } # Hone: Use `filter_map`
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# chars.each vs each_char
|
|
21
|
+
def iterate_chars(str)
|
|
22
|
+
count = 0
|
|
23
|
+
str.chars.each { |c| count += 1 } # Hone: Use `each_char`
|
|
24
|
+
count
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# map.flatten vs flat_map
|
|
28
|
+
def nested_map(arr)
|
|
29
|
+
arr.map { |x| [x, x * 2] }.flatten # Hone: Use `flat_map`
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# inject(:+) vs sum
|
|
33
|
+
def total(arr)
|
|
34
|
+
arr.inject(:+) # Hone: Use `sum`
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# shuffle.first vs sample
|
|
38
|
+
def random_element(arr)
|
|
39
|
+
arr.shuffle.first # Hone: Use `sample`
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# sort.first vs min
|
|
43
|
+
def smallest(arr)
|
|
44
|
+
arr.sort.first # Hone: Use `min`
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# reverse.first vs last
|
|
48
|
+
def final_element(arr)
|
|
49
|
+
arr.reverse.first # Hone: Use `last`
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# String concatenation in loop
|
|
53
|
+
def build_string(parts)
|
|
54
|
+
result = ""
|
|
55
|
+
parts.each do |part|
|
|
56
|
+
result += part # Hone: Use `<<` or array.join
|
|
57
|
+
end
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Variable from .chars then indexed
|
|
62
|
+
def char_at(str, i)
|
|
63
|
+
chars = str.chars # Allocates array
|
|
64
|
+
chars[i] # Hone: Use `str[i]` directly
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable all
|
|
4
|
+
# standardrb:disable all
|
|
5
|
+
|
|
6
|
+
# CPU-related patterns that Hone detects
|
|
7
|
+
# NOTE: This file intentionally contains anti-patterns for testing
|
|
8
|
+
|
|
9
|
+
class CpuPatterns
|
|
10
|
+
# positive? vs > 0
|
|
11
|
+
def check_positive(n)
|
|
12
|
+
n.positive? # Hone: Use `> 0` instead
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# loop vs while true
|
|
16
|
+
def count_up
|
|
17
|
+
i = 0
|
|
18
|
+
loop do # Hone: Use `while true` for JIT optimization
|
|
19
|
+
i += 1
|
|
20
|
+
break if i > 100
|
|
21
|
+
end
|
|
22
|
+
i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# each_with_index vs manual index
|
|
26
|
+
def sum_with_index(arr)
|
|
27
|
+
total = 0
|
|
28
|
+
arr.each_with_index do |val, i| # Hone: Consider manual indexing in hot loops
|
|
29
|
+
total += val * i
|
|
30
|
+
end
|
|
31
|
+
total
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Slice with length vs endless range
|
|
35
|
+
def tail(str)
|
|
36
|
+
str[1, str.length] # Hone: Use `str[1..]` instead
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# gsub single char vs tr
|
|
40
|
+
def replace_spaces(str)
|
|
41
|
+
str.gsub(" ", "_") # Hone: Use `tr` for single char replacement
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Regexp in loop
|
|
45
|
+
def match_pattern(strings)
|
|
46
|
+
strings.select do |s|
|
|
47
|
+
s.match?(/\d+/) # Hone: Extract regexp to constant
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable all
|
|
4
|
+
# standardrb:disable all
|
|
5
|
+
|
|
6
|
+
# JIT optimization patterns that Hone detects
|
|
7
|
+
# NOTE: This file intentionally contains anti-patterns for testing
|
|
8
|
+
# These patterns can prevent YJIT from optimizing effectively
|
|
9
|
+
|
|
10
|
+
class JitPatterns
|
|
11
|
+
# Dynamic instance_variable_set
|
|
12
|
+
def set_field(name, value)
|
|
13
|
+
instance_variable_set("@#{name}", value) # Hone: Causes shape transitions
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Dynamic instance_variable_get
|
|
17
|
+
def get_field(name)
|
|
18
|
+
instance_variable_get("@#{name}") # Hone: Consider explicit accessors
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Lazy ivar initialization outside initialize
|
|
22
|
+
def cached_value
|
|
23
|
+
@cached ||= expensive_computation # Hone: Initialize in constructor
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# defined? check for ivar
|
|
27
|
+
def ensure_loaded
|
|
28
|
+
@data = load_data unless defined?(@data) # Hone: Use nil check or initialize
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def expensive_computation
|
|
34
|
+
sleep(0.001)
|
|
35
|
+
42
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def load_data
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# RECOMMENDED: Initialize all ivars in constructor for stable object shape.
|
|
44
|
+
# This allows YJIT to optimize method dispatch because the object's
|
|
45
|
+
# memory layout is consistent from creation.
|
|
46
|
+
class StableShape
|
|
47
|
+
def initialize
|
|
48
|
+
@cached = nil
|
|
49
|
+
@data = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cached_value
|
|
53
|
+
@cached ||= expensive_computation
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def data
|
|
57
|
+
@data ||= load_data
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def expensive_computation
|
|
63
|
+
42
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_data
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
69
|
+
end
|
data/exe/hone
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Adapters
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(file_path)
|
|
7
|
+
@file_path = file_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def findings
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def build_finding(line:, message:, pattern_id:, optimization_type: :cpu, code: nil, speedup: nil)
|
|
17
|
+
Finding.new(
|
|
18
|
+
file: @file_path,
|
|
19
|
+
line: line,
|
|
20
|
+
column: 0,
|
|
21
|
+
pattern_id: pattern_id,
|
|
22
|
+
optimization_type: optimization_type,
|
|
23
|
+
source: source_name,
|
|
24
|
+
message: message,
|
|
25
|
+
speedup: speedup,
|
|
26
|
+
code: code
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def source_name
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Hone
|
|
6
|
+
module Adapters
|
|
7
|
+
class Fasterer < Base
|
|
8
|
+
def findings
|
|
9
|
+
return [] unless fasterer_available?
|
|
10
|
+
|
|
11
|
+
parse_output(run_fasterer)
|
|
12
|
+
rescue NotImplementedError => e
|
|
13
|
+
warn "[Hone::Adapters::Fasterer] #{e.message}"
|
|
14
|
+
[]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def source_name
|
|
20
|
+
:fasterer
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fasterer_available?
|
|
24
|
+
_, _, status = Open3.capture3("which", "fasterer")
|
|
25
|
+
status.success?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run_fasterer
|
|
29
|
+
stdout, _stderr, _status = Open3.capture3("fasterer", @file_path)
|
|
30
|
+
stdout
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_output(output)
|
|
34
|
+
raise NotImplementedError, "Fasterer output parsing not yet implemented"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Hone
|
|
7
|
+
module Adapters
|
|
8
|
+
class RubocopPerformance < Base
|
|
9
|
+
def findings
|
|
10
|
+
return [] unless rubocop_available? && rubocop_performance_available?
|
|
11
|
+
|
|
12
|
+
parse_output(run_rubocop)
|
|
13
|
+
rescue JSON::ParserError => e
|
|
14
|
+
warn "[Hone::Adapters::RubocopPerformance] Failed to parse RuboCop output: #{e.message}"
|
|
15
|
+
[]
|
|
16
|
+
rescue => e
|
|
17
|
+
warn "[Hone::Adapters::RubocopPerformance] #{e.message}"
|
|
18
|
+
[]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def source_name
|
|
24
|
+
:rubocop
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rubocop_available?
|
|
28
|
+
_, _, status = Open3.capture3("which", "rubocop")
|
|
29
|
+
status.success?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def rubocop_performance_available?
|
|
33
|
+
stdout, _, status = Open3.capture3("gem", "list", "rubocop-performance")
|
|
34
|
+
status.success? && stdout.include?("rubocop-performance")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def run_rubocop
|
|
38
|
+
stdout, _stderr, _status = Open3.capture3(
|
|
39
|
+
"rubocop",
|
|
40
|
+
"--only", "Performance",
|
|
41
|
+
"--format", "json",
|
|
42
|
+
@file_path
|
|
43
|
+
)
|
|
44
|
+
stdout
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_output(output)
|
|
48
|
+
return [] if output.nil? || output.strip.empty?
|
|
49
|
+
|
|
50
|
+
data = JSON.parse(output)
|
|
51
|
+
files = data["files"] || []
|
|
52
|
+
|
|
53
|
+
files.flat_map do |file_data|
|
|
54
|
+
offenses = file_data["offenses"] || []
|
|
55
|
+
offenses.map do |offense|
|
|
56
|
+
build_finding(
|
|
57
|
+
line: offense.dig("location", "line") || 0,
|
|
58
|
+
message: offense["message"],
|
|
59
|
+
pattern_id: offense["cop_name"]&.to_sym || :unknown,
|
|
60
|
+
optimization_type: severity_to_optimization_type(offense["severity"]),
|
|
61
|
+
code: extract_code(offense)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def severity_to_optimization_type(severity)
|
|
68
|
+
case severity
|
|
69
|
+
when "error", "fatal"
|
|
70
|
+
:cpu
|
|
71
|
+
when "warning"
|
|
72
|
+
:cpu
|
|
73
|
+
when "convention", "refactor"
|
|
74
|
+
:allocation
|
|
75
|
+
else
|
|
76
|
+
:cpu
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_code(offense)
|
|
81
|
+
offense.dig("location", "source")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|