hayaoki 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/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +118 -0
- data/exe/hayaoki +7 -0
- data/lib/hayaoki/boot_report/advisor.rb +113 -0
- data/lib/hayaoki/boot_report/aggregator.rb +367 -0
- data/lib/hayaoki/boot_report/formatters/json.rb +52 -0
- data/lib/hayaoki/boot_report/formatters/redact.rb +32 -0
- data/lib/hayaoki/boot_report/formatters/speedscope.rb +118 -0
- data/lib/hayaoki/boot_report/formatters/text.rb +275 -0
- data/lib/hayaoki/boot_report/gemfile.rb +146 -0
- data/lib/hayaoki/boot_report/report.rb +22 -0
- data/lib/hayaoki/boot_report/rules/base.rb +113 -0
- data/lib/hayaoki/boot_report/rules/bootsnap_ineffective.rb +88 -0
- data/lib/hayaoki/boot_report/rules/controller_action_methods.rb +33 -0
- data/lib/hayaoki/boot_report/rules/dev_gems_in_production.rb +80 -0
- data/lib/hayaoki/boot_report/rules/devise_reload_routes.rb +43 -0
- data/lib/hayaoki/boot_report/rules/eager_load_tradeoff.rb +32 -0
- data/lib/hayaoki/boot_report/rules/heavy_app_initializer.rb +59 -0
- data/lib/hayaoki/boot_report/rules/tzinfo_eager_load.rb +33 -0
- data/lib/hayaoki/boot_report/rules/unused_heavy_gem.rb +149 -0
- data/lib/hayaoki/boot_report/runner.rb +472 -0
- data/lib/hayaoki/cli.rb +210 -0
- data/lib/hayaoki/probe.rb +513 -0
- data/lib/hayaoki/version.rb +5 -0
- data/lib/hayaoki.rb +28 -0
- metadata +70 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# hayaoki boot-report の probe (子プロセス側)。設計: docs/design/probe.md
|
|
4
|
+
#
|
|
5
|
+
# ruby -r/path/to/hayaoki/probe.rb -e 'require "./config/environment"'
|
|
6
|
+
#
|
|
7
|
+
# - 依存ゼロ (json も require しない)。Ruby 3.2 の構文で書く。
|
|
8
|
+
# - require 系は alias chain で包む (Kernel には prepend しない。§1.3, §1.4)。
|
|
9
|
+
# - Rails 関連 (H4〜H9) は対象ファイルの require 直後に遅延 prepend する (§2.2)。
|
|
10
|
+
# - 結果は ENV["HAYAOKI_PROBE_OUT"] のパスへ tmp + rename で書く (§5)。
|
|
11
|
+
return if defined?(::Hayaoki::Probe::INSTALLED)
|
|
12
|
+
|
|
13
|
+
module Hayaoki; end
|
|
14
|
+
|
|
15
|
+
module Hayaoki::Probe
|
|
16
|
+
CLOCK = Process::CLOCK_MONOTONIC
|
|
17
|
+
T0 = Process.clock_gettime(CLOCK)
|
|
18
|
+
T0_REAL = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
19
|
+
CPU0 = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
|
20
|
+
PID = Process.pid
|
|
21
|
+
OUT = ENV.delete("HAYAOKI_PROBE_OUT")
|
|
22
|
+
SPAWN_AT = ENV.delete("HAYAOKI_SPAWN_AT")
|
|
23
|
+
PRE_FEATURES = $LOADED_FEATURES.size
|
|
24
|
+
GC_COUNT0 = GC.count
|
|
25
|
+
GC_TIME0 = GC.stat(:time)
|
|
26
|
+
|
|
27
|
+
# 1 span = W スロット: [kind, name, parent_idx, t_start, t_end, res, tid, cpu]
|
|
28
|
+
W = 8
|
|
29
|
+
EV = []
|
|
30
|
+
MAIN = Thread.main
|
|
31
|
+
MAIN_STACK = []
|
|
32
|
+
# メイン以外のスレッドは専用バッファ (thread_variable) に記録し、write! で EV の後ろに結合する。
|
|
33
|
+
BUFFERS = []
|
|
34
|
+
BUF_LOCK = Mutex.new
|
|
35
|
+
K_REQ = 0; K_RR = 1; K_LOAD = 2; K_INIT = 3; K_ONLOAD = 4; K_HOOKS = 5; K_EAGER = 6; K_PHASE = 7
|
|
36
|
+
BUILTIN_REQUIRE = Kernel.private_method_defined?(:gem_original_require) ? :gem_original_require : :__hayaoki_require
|
|
37
|
+
|
|
38
|
+
@on = true
|
|
39
|
+
@false_count = 0
|
|
40
|
+
# resolve_feature が名前から feature を見つけられず $LOADED_FEATURES.last を使った回数と例 (先頭 5 件)。
|
|
41
|
+
# 他スレッドの require が末尾にあると帰属を間違えうるので、件数を出して気付けるようにする
|
|
42
|
+
@fallbacks = 0
|
|
43
|
+
@fallback_names = []
|
|
44
|
+
@false_time = 0.0
|
|
45
|
+
@boot_end = nil
|
|
46
|
+
@errors = []
|
|
47
|
+
@warnings = []
|
|
48
|
+
@warnings << "gem_original_require not found; delegating require_relative to the builtin require" if BUILTIN_REQUIRE == :__hayaoki_require && defined?(::Gem::Specification)
|
|
49
|
+
|
|
50
|
+
# --- recorder (ホットパス。ブロックを使わずフレームも増やさない) -----------------
|
|
51
|
+
|
|
52
|
+
# span を開く。記録しない場合は nil を返す (その場合 leave は呼ばない)。
|
|
53
|
+
def self.enter(kind, name)
|
|
54
|
+
return unless @on
|
|
55
|
+
t = Thread.current
|
|
56
|
+
if t.equal?(MAIN)
|
|
57
|
+
st = MAIN_STACK
|
|
58
|
+
ev = EV
|
|
59
|
+
tid = 0
|
|
60
|
+
else
|
|
61
|
+
st = t.thread_variable_get(:__hayaoki_stack) || t.thread_variable_set(:__hayaoki_stack, [])
|
|
62
|
+
ev = t.thread_variable_get(:__hayaoki_buf) || new_buffer(t)
|
|
63
|
+
tid = t.object_id
|
|
64
|
+
end
|
|
65
|
+
parent = st.last
|
|
66
|
+
# alias chain のラッパーを同一引数のまま二重に通った場合は記録しない (§3.4)
|
|
67
|
+
return if parent && kind <= K_LOAD && ev[parent] <= K_LOAD && ev[parent + 1].equal?(name) && ev[parent + 4].nil? && ev.size == parent + W
|
|
68
|
+
i = ev.size
|
|
69
|
+
ev.push(kind, name, parent || -1, Process.clock_gettime(CLOCK), nil, nil, tid, nil)
|
|
70
|
+
st.push(i)
|
|
71
|
+
i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# スレッド専用バッファを作る。初回だけ Mutex で全体のリストに登録する。
|
|
75
|
+
def self.new_buffer(th)
|
|
76
|
+
buf = []
|
|
77
|
+
th.thread_variable_set(:__hayaoki_buf, buf)
|
|
78
|
+
BUF_LOCK.synchronize { BUFFERS << buf }
|
|
79
|
+
buf
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# span を閉じる。res: require 系は feature / false / :raised、それ以外は meta / :raised。
|
|
83
|
+
def self.leave(i, kind, res, cpu0 = nil)
|
|
84
|
+
t = Process.clock_gettime(CLOCK)
|
|
85
|
+
th = Thread.current
|
|
86
|
+
main = th.equal?(MAIN)
|
|
87
|
+
st = main ? MAIN_STACK : th.thread_variable_get(:__hayaoki_stack)
|
|
88
|
+
ev = main ? EV : th.thread_variable_get(:__hayaoki_buf)
|
|
89
|
+
st.pop
|
|
90
|
+
if false.equal?(res) && kind <= K_LOAD && ev.size == i + W
|
|
91
|
+
# false を返した require は span として残さず、回数と時間だけ数える (§3.5)
|
|
92
|
+
BUF_LOCK.synchronize do
|
|
93
|
+
@false_time += t - ev[i + 3]
|
|
94
|
+
@false_count += 1
|
|
95
|
+
end
|
|
96
|
+
ev.pop(W)
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
ev[i + 4] = t
|
|
100
|
+
ev[i + 5] = res
|
|
101
|
+
ev[i + 7] = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) - cpu0 if cpu0
|
|
102
|
+
@boot_end ||= t if main && st.empty? # 最初に閉じた深さ0 (= entry)。at_exit 内の require で延ばさない
|
|
103
|
+
after_require(res) if kind <= K_RR && res.is_a?(String) && !PENDING.empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 粗い span 用のブロック版 (initializer / on_load / eager_load / phase)。数百件だけで使う。
|
|
107
|
+
def self.span(kind, name, meta = nil)
|
|
108
|
+
return yield unless @on
|
|
109
|
+
i = enter(kind, name)
|
|
110
|
+
return yield unless i
|
|
111
|
+
cpu0 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
|
|
112
|
+
ok = false
|
|
113
|
+
begin
|
|
114
|
+
r = yield
|
|
115
|
+
ok = true
|
|
116
|
+
r
|
|
117
|
+
ensure
|
|
118
|
+
leave(i, kind, ok ? meta : :raised, cpu0)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- feature の解決 -----------------------------------------------------------
|
|
123
|
+
# 他スレッドが同時に require すると $LOADED_FEATURES.last は自分のものとは限らない。
|
|
124
|
+
# 速い方法は last が name に一致する場合、遅い方法は末尾から最大 SCAN_MAX 件を逆走査、
|
|
125
|
+
# 見つからなければ last にフォールバックする (従来の挙動)。
|
|
126
|
+
SCAN_MAX = 200
|
|
127
|
+
|
|
128
|
+
# f が name の feature か。name は絶対パス (拡張子なしも可) か相対名。文字列は(ほぼ)割り当てない。
|
|
129
|
+
def self.feature_match?(f, name)
|
|
130
|
+
return false unless f.is_a?(String)
|
|
131
|
+
n = name.bytesize
|
|
132
|
+
fb = f.bytesize
|
|
133
|
+
if name.getbyte(0) == 47 # 絶対パス: 完全一致、または name + ".ext"
|
|
134
|
+
return fb == n ? f == name : (fb > n && fb - n <= 7 && f.getbyte(n) == 46 && f.start_with?(name) && !f.byteindex("/", n))
|
|
135
|
+
end
|
|
136
|
+
if name.getbyte(0) == 46 # "./x" や "../../x" は先頭の "./" と "../" を取り、"/x" の形で比べる
|
|
137
|
+
k = 0
|
|
138
|
+
while name.getbyte(k) == 46
|
|
139
|
+
if name.getbyte(k + 1) == 47 then k += 2
|
|
140
|
+
elsif name.getbyte(k + 1) == 46 && name.getbyte(k + 2) == 47 then k += 3
|
|
141
|
+
else break
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
if k.positive?
|
|
145
|
+
name = name.byteslice(k - 1, n - k + 1) # 直前の "/" を含める
|
|
146
|
+
n = name.bytesize
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
# require "psych.so" は macOS では psych.bundle を読む (Ruby が .so を DLEXT に読み替える)。".so" を外して比べる
|
|
150
|
+
if n > 3 && name.end_with?(".so") && (f.end_with?(".bundle") || f.end_with?(".dll"))
|
|
151
|
+
name = name.byteslice(0, n - 3)
|
|
152
|
+
n -= 3
|
|
153
|
+
end
|
|
154
|
+
# name が "/" で始まるなら区切りは name 自身に含まれる。そうでなければ直前が "/" であること
|
|
155
|
+
sep = name.getbyte(0) == 47
|
|
156
|
+
return true if !sep && fb == n && f == name
|
|
157
|
+
# 「/name」で終わる (name が拡張子付き) か「/name.ext」で終わる
|
|
158
|
+
return true if f.end_with?(name) && (sep || (fb > n && f.getbyte(fb - n - 1) == 47))
|
|
159
|
+
dot = f.byterindex(".")
|
|
160
|
+
# dot == n は feature 自体が "name.ext" の場合 (Ruby に組み込まれた拡張は "prism/prism.so" のように登録される)
|
|
161
|
+
return false unless dot && dot >= n && (sep || dot == n || f.getbyte(dot - n - 1) == 47)
|
|
162
|
+
f.byteindex(name, dot - n) == dot - n
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.resolve_feature(name)
|
|
166
|
+
name = name.to_path if !name.is_a?(String) && name.respond_to?(:to_path)
|
|
167
|
+
return $LOADED_FEATURES.last unless name.is_a?(String)
|
|
168
|
+
fl = $LOADED_FEATURES
|
|
169
|
+
last = fl.last
|
|
170
|
+
return last if feature_match?(last, name)
|
|
171
|
+
i = fl.size - 2
|
|
172
|
+
stop = fl.size - SCAN_MAX
|
|
173
|
+
stop = 0 if stop < 0
|
|
174
|
+
while i >= stop
|
|
175
|
+
f = fl[i]
|
|
176
|
+
return f if feature_match?(f, name)
|
|
177
|
+
i -= 1
|
|
178
|
+
end
|
|
179
|
+
@fallbacks += 1
|
|
180
|
+
@fallback_names << name if @fallback_names.size < 5
|
|
181
|
+
last
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# --- 遅延インストール (§2.2) --------------------------------------------------
|
|
185
|
+
SELF_PATH = File.expand_path(__FILE__).freeze
|
|
186
|
+
|
|
187
|
+
PENDING = {
|
|
188
|
+
"/rails/initializable.rb" => :install_initializer,
|
|
189
|
+
"/active_support/lazy_load_hooks.rb" => :install_on_load,
|
|
190
|
+
"/zeitwerk/loader.rb" => :install_eager_load,
|
|
191
|
+
"/bundler/runtime.rb" => :install_bundler,
|
|
192
|
+
"/rails/application.rb" => :install_application,
|
|
193
|
+
"/active_support/deprecation/reporting.rb" => :install_deprecation,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# require が true を返した直後に呼ぶ。$LOADED_FEATURES.last (= res) の末尾一致で判定する。
|
|
197
|
+
def self.after_require(feature)
|
|
198
|
+
PENDING.each do |suffix, installer|
|
|
199
|
+
next unless feature.end_with?(suffix)
|
|
200
|
+
PENDING.delete(suffix)
|
|
201
|
+
run_installer(installer)
|
|
202
|
+
break
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.run_installer(installer)
|
|
207
|
+
send(installer)
|
|
208
|
+
rescue Exception => e # インストールの失敗で boot を止めない
|
|
209
|
+
@errors << "#{installer}: #{e.class}: #{e.message}"[0, 500]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.install_initializer = ::Rails::Initializable::Initializer.prepend(InitializerHook)
|
|
213
|
+
def self.install_on_load = ::ActiveSupport::LazyLoadHooks.prepend(LoadHooksHook)
|
|
214
|
+
def self.install_eager_load = ::Zeitwerk::Loader.prepend(EagerLoadHook)
|
|
215
|
+
def self.install_bundler = ::Bundler::Runtime.prepend(BundlerHook)
|
|
216
|
+
def self.install_application = ::Rails::Application.prepend(AppHook)
|
|
217
|
+
def self.install_deprecation = ::ActiveSupport::Deprecation::Reporting.prepend(DeprecationHook)
|
|
218
|
+
|
|
219
|
+
# probe が後から読み込まれた場合に備え、定義済みのものは即座にインストールする
|
|
220
|
+
def self.install_defined
|
|
221
|
+
{
|
|
222
|
+
"/rails/initializable.rb" => -> { defined?(::Rails::Initializable::Initializer) },
|
|
223
|
+
"/active_support/lazy_load_hooks.rb" => -> { defined?(::ActiveSupport::LazyLoadHooks) },
|
|
224
|
+
"/zeitwerk/loader.rb" => -> { defined?(::Zeitwerk::Loader) },
|
|
225
|
+
"/bundler/runtime.rb" => -> { defined?(::Bundler::Runtime) },
|
|
226
|
+
"/rails/application.rb" => -> { defined?(::Rails::Application) },
|
|
227
|
+
"/active_support/deprecation/reporting.rb" => -> { defined?(::ActiveSupport::Deprecation::Reporting) },
|
|
228
|
+
}.each do |suffix, check|
|
|
229
|
+
next unless PENDING.key?(suffix)
|
|
230
|
+
ok = begin
|
|
231
|
+
check.call
|
|
232
|
+
rescue Exception
|
|
233
|
+
false
|
|
234
|
+
end
|
|
235
|
+
run_installer(PENDING.delete(suffix)) if ok
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def self.loc(block)
|
|
240
|
+
l = block.source_location
|
|
241
|
+
l && "#{l[0]}:#{l[1]}"
|
|
242
|
+
rescue StandardError
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def self.label(obj)
|
|
247
|
+
(obj.is_a?(Module) ? obj.name : obj.class.name).to_s
|
|
248
|
+
rescue StandardError
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# --- Rails 関連のフック (prepend 用) -----------------------------------------
|
|
253
|
+
module InitializerHook
|
|
254
|
+
def run(*args)
|
|
255
|
+
meta = begin
|
|
256
|
+
[@context.class.name, Hayaoki::Probe.loc(block)]
|
|
257
|
+
rescue StandardError
|
|
258
|
+
nil
|
|
259
|
+
end
|
|
260
|
+
Hayaoki::Probe.hook_eager_namespaces if name == :eager_load!
|
|
261
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_INIT, name.to_s, meta) { super }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Finisher の eager_load! initializer は Zeitwerk の後で config.eager_load_namespaces.each(&:eager_load!) と
|
|
266
|
+
# Rails.eager_load! を呼ぶ。TZInfo.eager_load! (zoneinfo を全部読む) のような require を伴わない重い処理が
|
|
267
|
+
# ここに入るので、namespace ごとに K_EAGER span を作る。meta は元の eager_load! の定義位置 (持ち主の判定用)。
|
|
268
|
+
module NamespaceEagerLoadHook
|
|
269
|
+
def eager_load!(...)
|
|
270
|
+
name = Hayaoki::Probe.label(self)
|
|
271
|
+
# ActiveSupport::Autoload#eager_load! や Engine の delegate だと定義位置が共通の場所になるので、
|
|
272
|
+
# 定数を定義したファイルを優先し、取れなければ元の eager_load! の位置を使う。
|
|
273
|
+
loc = begin
|
|
274
|
+
l = (Object.const_source_location(name) if name && !name.empty?)
|
|
275
|
+
l = method(:eager_load!).super_method&.source_location if l.nil? || l.empty?
|
|
276
|
+
l && !l.empty? ? "#{l[0]}:#{l[1]}" : nil
|
|
277
|
+
rescue StandardError, NameError
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_EAGER, "#{name}.eager_load!", loc) { super }
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def self.hook_eager_namespaces
|
|
285
|
+
list = ::Rails.application.config.eager_load_namespaces.to_a
|
|
286
|
+
list << ::Rails
|
|
287
|
+
list.uniq.each do |ns|
|
|
288
|
+
next unless ns.respond_to?(:eager_load!)
|
|
289
|
+
sc = ns.singleton_class
|
|
290
|
+
sc.prepend(NamespaceEagerLoadHook) unless sc.include?(NamespaceEagerLoadHook)
|
|
291
|
+
end
|
|
292
|
+
rescue Exception => e # 計測の失敗で boot を止めない
|
|
293
|
+
@errors << "eager_load_namespaces: #{e.class}: #{e.message}"[0, 500]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
module LoadHooksHook
|
|
297
|
+
def run_load_hooks(name, base = Object)
|
|
298
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_HOOKS, name.to_s, Hayaoki::Probe.label(base)) { super }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private def execute_hook(name, base, options, block)
|
|
302
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_ONLOAD, name.to_s, Hayaoki::Probe.loc(block)) { super }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
module EagerLoadHook
|
|
307
|
+
def eager_load(...)
|
|
308
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_EAGER, tag.to_s, nil) { super }
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
module BundlerHook
|
|
313
|
+
def require(*groups)
|
|
314
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_PHASE, "bundler_require", groups) { super }
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# 非推奨警告の "called from" は Rails 自身のフレームを飛ばした最初のフレームになる。probe のフック
|
|
319
|
+
# (initializer の run など) がそこに入ると警告の表示が変わるので、probe のファイルも飛ばさせる。
|
|
320
|
+
# Rails 7.0 は ignored_callstack、7.1 以降は ignored_callstack?。
|
|
321
|
+
module DeprecationHook
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def ignored_callstack(path)
|
|
325
|
+
path.to_s.start_with?(Hayaoki::Probe::SELF_PATH) || super
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def ignored_callstack?(path)
|
|
329
|
+
path.to_s.start_with?(Hayaoki::Probe::SELF_PATH) || super
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
module AppHook
|
|
334
|
+
def initialize!(...)
|
|
335
|
+
Hayaoki::Probe.span(Hayaoki::Probe::K_PHASE, "initialize", nil) { super }
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# --- 出力 (§5) ---------------------------------------------------------------
|
|
340
|
+
ESC = { '"' => '\\"', "\\" => "\\\\", "\n" => "\\n", "\r" => "\\r", "\t" => "\\t" }.freeze
|
|
341
|
+
|
|
342
|
+
NEEDS_ESCAPE = /["\\\x00-\x1f]/
|
|
343
|
+
|
|
344
|
+
# 自前の最小 JSON エンコーダ。対象は String / Symbol / Integer / Float / nil / true / false / Array / Hash。
|
|
345
|
+
# 文字列の大半はエスケープ不要なので、確認だけして引用符で囲む速い方法を先に試す。
|
|
346
|
+
def self.encode(v)
|
|
347
|
+
case v
|
|
348
|
+
when String
|
|
349
|
+
s = v.encoding == Encoding::UTF_8 ? v : v.dup.force_encoding(Encoding::UTF_8)
|
|
350
|
+
if s.valid_encoding?
|
|
351
|
+
return "\"#{s}\"" unless s.match?(NEEDS_ESCAPE)
|
|
352
|
+
else
|
|
353
|
+
s = s.scrub
|
|
354
|
+
end
|
|
355
|
+
"\"#{s.gsub(/["\\\u0000-\u001f]/) { |c| ESC[c] || format("\\u%04x", c.ord) }}\""
|
|
356
|
+
when Symbol then encode(v.to_s)
|
|
357
|
+
when Integer then v.to_s
|
|
358
|
+
when Float then v.finite? ? v.to_s : "null"
|
|
359
|
+
when nil then "null"
|
|
360
|
+
when true, false then v.to_s
|
|
361
|
+
when Array
|
|
362
|
+
return "[#{v.join(",")}]" if v.all?(Integer) # Integer だけの配列はまとめて連結する
|
|
363
|
+
"[#{v.map { |x| encode(x) }.join(",")}]"
|
|
364
|
+
when Hash then "{#{v.map { |k, x| "#{encode(k.to_s)}:#{encode(x)}" }.join(",")}}"
|
|
365
|
+
else encode(v.to_s)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def self.us(t) = ((t - T0) * 1_000_000).round
|
|
370
|
+
|
|
371
|
+
# EV の後ろに各スレッドのバッファをオフセット付きで結合する (親の添字はバッファ内なのでずらす)。
|
|
372
|
+
def self.events
|
|
373
|
+
bufs = BUF_LOCK.synchronize { BUFFERS.dup }
|
|
374
|
+
out = []
|
|
375
|
+
base = 0
|
|
376
|
+
[EV, *bufs].each do |ev|
|
|
377
|
+
n = ev.size - ev.size % W
|
|
378
|
+
j = 0
|
|
379
|
+
while j < n
|
|
380
|
+
par = ev[j + 2]
|
|
381
|
+
out.push(
|
|
382
|
+
ev[j], ev[j + 1].to_s, par < 0 ? par : par + base, us(ev[j + 3]), ev[j + 4] && us(ev[j + 4]),
|
|
383
|
+
ev[j + 5], ev[j + 6], ev[j + 7] && (ev[j + 7] * 1_000_000).round
|
|
384
|
+
)
|
|
385
|
+
j += W
|
|
386
|
+
end
|
|
387
|
+
base += n
|
|
388
|
+
end
|
|
389
|
+
out
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def self.specs
|
|
393
|
+
return {} unless defined?(::Gem) && ::Gem.respond_to?(:loaded_specs)
|
|
394
|
+
::Gem.loaded_specs.to_h do |name, s|
|
|
395
|
+
# extension_dir は gem ごとに約 0.3ms かかるため、ネイティブ拡張を持つ gem だけ解決する
|
|
396
|
+
ext = begin
|
|
397
|
+
s.extensions.empty? ? nil : s.extension_dir
|
|
398
|
+
rescue StandardError
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
[name, [s.version.to_s, s.full_gem_path, ext]]
|
|
402
|
+
end
|
|
403
|
+
rescue StandardError => e
|
|
404
|
+
@errors << "specs: #{e.class}: #{e.message}"[0, 500]
|
|
405
|
+
{}
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def self.rails_root
|
|
409
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:root)
|
|
410
|
+
::Rails.root&.to_s
|
|
411
|
+
rescue StandardError
|
|
412
|
+
nil
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def self.payload(status, error, now)
|
|
416
|
+
own = $LOADED_FEATURES.count { |f| f.end_with?("/hayaoki/probe.rb") }
|
|
417
|
+
grew = $LOADED_FEATURES.size - PRE_FEATURES - own
|
|
418
|
+
if grew > 0 && @false_count == 0 && EV.empty? && BUFFERS.all?(&:empty?)
|
|
419
|
+
@warnings << "$LOADED_FEATURES grew by #{grew} but no require was recorded (require may be overridden without alias_method)"
|
|
420
|
+
end
|
|
421
|
+
{
|
|
422
|
+
v: 1, status: status, error: error, pid: PID, ruby: RUBY_VERSION, platform: RUBY_PLATFORM,
|
|
423
|
+
t0_real: T0_REAL, spawn_at: SPAWN_AT, cpu0_us: (CPU0 * 1_000_000).round,
|
|
424
|
+
boot_end_us: @boot_end && us(@boot_end), exit_us: us(now),
|
|
425
|
+
pre_features: PRE_FEATURES, features: $LOADED_FEATURES.size,
|
|
426
|
+
gc: { count0: GC_COUNT0, count1: GC.count, time0_ms: GC_TIME0, time1_ms: GC.stat(:time) },
|
|
427
|
+
false_require: { count: @false_count, time_us: (@false_time * 1_000_000).round },
|
|
428
|
+
resolve_fallbacks: { count: @fallbacks, examples: @fallback_names },
|
|
429
|
+
width: W, events: events, specs: specs, rails_root: rails_root,
|
|
430
|
+
pwd_realpath: (File.realpath(Dir.pwd) rescue nil),
|
|
431
|
+
libdir: defined?(::RbConfig) ? [::RbConfig::CONFIG["rubylibdir"], ::RbConfig::CONFIG["rubyarchdir"]].compact : [],
|
|
432
|
+
probe_errors: @errors, probe_warnings: @warnings, pending_hooks: PENDING.keys,
|
|
433
|
+
}
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# pid ガード付きで tmp に書いてから rename する。
|
|
437
|
+
def self.write!
|
|
438
|
+
return unless Process.pid == PID && OUT
|
|
439
|
+
@on = false # 書き出し中の autoload などを記録しない
|
|
440
|
+
now = Process.clock_gettime(CLOCK)
|
|
441
|
+
err = $!
|
|
442
|
+
ok = err.nil? || (err.is_a?(SystemExit) && err.success?)
|
|
443
|
+
error = ok ? nil : { class: err.class.name, message: err.message.to_s[0, 2000], backtrace: Array(err.backtrace).first(20) }
|
|
444
|
+
data = encode(payload(ok ? "ok" : "error", error, now))
|
|
445
|
+
File.write("#{OUT}.tmp", data)
|
|
446
|
+
File.rename("#{OUT}.tmp", OUT)
|
|
447
|
+
rescue Exception => e # at_exit で例外を出して終了コードを変えない
|
|
448
|
+
begin
|
|
449
|
+
$stderr.puts "[hayaoki probe] write failed: #{e.class}: #{e.message}"
|
|
450
|
+
rescue StandardError
|
|
451
|
+
nil
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# --- Kernel (alias chain。prepend は使わない) -------------------------------------
|
|
457
|
+
[::Kernel, ::Kernel.singleton_class].each do |k|
|
|
458
|
+
k.class_eval do
|
|
459
|
+
alias_method :__hayaoki_require, :require
|
|
460
|
+
alias_method :require, :require # -w での再定義警告を抑止 (bootsnap と同じ手法)
|
|
461
|
+
def require(path)
|
|
462
|
+
i = Hayaoki::Probe.enter(0, path)
|
|
463
|
+
ok = false
|
|
464
|
+
r = __hayaoki_require(path)
|
|
465
|
+
ok = true
|
|
466
|
+
r
|
|
467
|
+
ensure
|
|
468
|
+
Hayaoki::Probe.leave(i, 0, ok ? (r && Hayaoki::Probe.resolve_feature(path)) : :raised) if i
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
alias_method :__hayaoki_load, :load
|
|
472
|
+
alias_method :load, :load
|
|
473
|
+
def load(path, wrap = false)
|
|
474
|
+
i = Hayaoki::Probe.enter(2, path)
|
|
475
|
+
ok = false
|
|
476
|
+
r = __hayaoki_load(path, wrap)
|
|
477
|
+
ok = true
|
|
478
|
+
r
|
|
479
|
+
ensure
|
|
480
|
+
Hayaoki::Probe.leave(i, 2, ok ? path.to_s : :raised) if i
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# require_relative は Kernel に直接定義し直す。元のメソッドは基準パスが正しく解決されないので呼ばず、
|
|
486
|
+
# gem_original_require (無ければ builtin の退避先) に委譲する (§1.4)。
|
|
487
|
+
::Kernel.module_eval do
|
|
488
|
+
alias_method :require_relative, :require_relative
|
|
489
|
+
def require_relative(path)
|
|
490
|
+
loc = caller_locations(1, 1)&.first
|
|
491
|
+
base = loc&.absolute_path
|
|
492
|
+
# eval(code, binding, file) で実行されたコードは absolute_path が nil になる。ネイティブと同じく、渡された
|
|
493
|
+
# file を (相対ならカレントディレクトリ基準で) 使う。-e も同じ扱い。file を渡さない eval だけ失敗させる
|
|
494
|
+
# (Redmine は lib/plugins/*/init.rb をこの形で読む)
|
|
495
|
+
base ||= File.expand_path(loc.path) if loc&.path && !loc.path.start_with?("(eval")
|
|
496
|
+
raise LoadError, "cannot infer basepath" unless base
|
|
497
|
+
abs = File.expand_path(path, File.dirname(base))
|
|
498
|
+
i = Hayaoki::Probe.enter(1, abs)
|
|
499
|
+
ok = false
|
|
500
|
+
r = __send__(Hayaoki::Probe::BUILTIN_REQUIRE, abs)
|
|
501
|
+
ok = true
|
|
502
|
+
r
|
|
503
|
+
ensure
|
|
504
|
+
Hayaoki::Probe.leave(i, 1, ok ? (r && Hayaoki::Probe.resolve_feature(abs)) : :raised) if i
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
private :require, :load, :require_relative, :__hayaoki_require, :__hayaoki_load
|
|
508
|
+
end
|
|
509
|
+
::Kernel.singleton_class.send(:private, :__hayaoki_require, :__hayaoki_load)
|
|
510
|
+
|
|
511
|
+
Hayaoki::Probe.install_defined
|
|
512
|
+
at_exit { Hayaoki::Probe.write! }
|
|
513
|
+
Hayaoki::Probe::INSTALLED = true
|
data/lib/hayaoki.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hayaoki/version"
|
|
4
|
+
|
|
5
|
+
# Hayaoki - Rails cold-start toolkit
|
|
6
|
+
module Hayaoki
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module Hayaoki
|
|
11
|
+
autoload :CLI, "hayaoki/cli"
|
|
12
|
+
|
|
13
|
+
module BootReport
|
|
14
|
+
autoload :Runner, "hayaoki/boot_report/runner"
|
|
15
|
+
autoload :RunResult, "hayaoki/boot_report/runner"
|
|
16
|
+
autoload :RunError, "hayaoki/boot_report/runner"
|
|
17
|
+
autoload :Aggregator, "hayaoki/boot_report/aggregator"
|
|
18
|
+
autoload :Report, "hayaoki/boot_report/report"
|
|
19
|
+
autoload :Advisor, "hayaoki/boot_report/advisor"
|
|
20
|
+
autoload :Suggestion, "hayaoki/boot_report/advisor"
|
|
21
|
+
|
|
22
|
+
module Formatters
|
|
23
|
+
autoload :Text, "hayaoki/boot_report/formatters/text"
|
|
24
|
+
autoload :Json, "hayaoki/boot_report/formatters/json"
|
|
25
|
+
autoload :Speedscope, "hayaoki/boot_report/formatters/speedscope"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hayaoki
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Takato Hano
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: hayaoki boot-report boots a Rails app in a child process with a probe
|
|
13
|
+
and breaks boot time down by gem, initializer, on_load hook and eager loading, then
|
|
14
|
+
suggests measured fixes. Built for cold starts on serverless and containers.
|
|
15
|
+
executables:
|
|
16
|
+
- hayaoki
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- exe/hayaoki
|
|
24
|
+
- lib/hayaoki.rb
|
|
25
|
+
- lib/hayaoki/boot_report/advisor.rb
|
|
26
|
+
- lib/hayaoki/boot_report/aggregator.rb
|
|
27
|
+
- lib/hayaoki/boot_report/formatters/json.rb
|
|
28
|
+
- lib/hayaoki/boot_report/formatters/redact.rb
|
|
29
|
+
- lib/hayaoki/boot_report/formatters/speedscope.rb
|
|
30
|
+
- lib/hayaoki/boot_report/formatters/text.rb
|
|
31
|
+
- lib/hayaoki/boot_report/gemfile.rb
|
|
32
|
+
- lib/hayaoki/boot_report/report.rb
|
|
33
|
+
- lib/hayaoki/boot_report/rules/base.rb
|
|
34
|
+
- lib/hayaoki/boot_report/rules/bootsnap_ineffective.rb
|
|
35
|
+
- lib/hayaoki/boot_report/rules/controller_action_methods.rb
|
|
36
|
+
- lib/hayaoki/boot_report/rules/dev_gems_in_production.rb
|
|
37
|
+
- lib/hayaoki/boot_report/rules/devise_reload_routes.rb
|
|
38
|
+
- lib/hayaoki/boot_report/rules/eager_load_tradeoff.rb
|
|
39
|
+
- lib/hayaoki/boot_report/rules/heavy_app_initializer.rb
|
|
40
|
+
- lib/hayaoki/boot_report/rules/tzinfo_eager_load.rb
|
|
41
|
+
- lib/hayaoki/boot_report/rules/unused_heavy_gem.rb
|
|
42
|
+
- lib/hayaoki/boot_report/runner.rb
|
|
43
|
+
- lib/hayaoki/cli.rb
|
|
44
|
+
- lib/hayaoki/probe.rb
|
|
45
|
+
- lib/hayaoki/version.rb
|
|
46
|
+
homepage: https://github.com/TakatoHano/hayaoki
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
source_code_uri: https://github.com/TakatoHano/hayaoki
|
|
51
|
+
changelog_uri: https://github.com/TakatoHano/hayaoki/blob/main/CHANGELOG.md
|
|
52
|
+
rubygems_mfa_required: 'true'
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.2'
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 4.0.16
|
|
68
|
+
specification_version: 4
|
|
69
|
+
summary: Find out where your Rails app spends its boot time, and what to change
|
|
70
|
+
test_files: []
|