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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hayaoki
4
+ VERSION = "0.1.0"
5
+ end
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: []