1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
# frozen_string_literal: true
require "test_prof/ext/float_duration"
module TestProf
# Make DB fixtures from blocks.
module AnyFixture
INSERT_RXP = /^INSERT INTO ([\S]+)/.freeze
using FloatDuration
class Cache # :nodoc:
attr_reader :store, :stats
def initialize
@store = {}
@stats = {}
end
def fetch(key)
if store.key?(key)
stats[key][:hit] += 1
return store[key]
end
return unless block_given?
ts = TestProf.now
store[key] = yield
stats[key] = {time: TestProf.now - ts, hit: 0}
store[key]
end
def clear
store.clear
stats.clear
end
end
class << self
include Logging
attr_accessor :reporting_enabled
def reporting_enabled?
reporting_enabled == true
end
# Register a block of code as a fixture,
# returns the result of the block execution
def register(id)
cache.fetch(id) do
ActiveSupport::Notifications.subscribed(method(:subscriber), "sql.active_record") do
yield
end
end
end
# Clean all affected tables (but do not reset cache)
def clean
disable_referential_integrity do
tables_cache.keys.reverse_each do |table|
ActiveRecord::Base.connection.execute %(
DELETE FROM #{table}
)
end
end
end
# Reset all information and clean tables
def reset
clean
tables_cache.clear
cache.clear
end
def subscriber(_event, _start, _finish, _id, data)
matches = data.fetch(:sql).match(INSERT_RXP)
tables_cache[matches[1]] = true if matches
end
def report_stats
if cache.stats.empty?
log :info, "AnyFixture has not been used"
return
end
msgs = []
msgs <<
<<~MSG
AnyFixture usage stats:
MSG
first_column = cache.stats.keys.map(&:size).max + 2
msgs << format(
"%#{first_column}s %12s %9s %12s",
"key", "build time", "hit count", "saved time"
)
msgs << ""
total_spent = 0.0
total_saved = 0.0
total_miss = 0.0
cache.stats.to_a.sort_by { |(_, v)| -v[:hit] }.each do |(key, stats)|
total_spent += stats[:time]
saved = stats[:time] * stats[:hit]
total_saved += saved
total_miss += stats[:time] if stats[:hit].zero?
msgs << format(
"%#{first_column}s %12s %9d %12s",
key, stats[:time].duration, stats[:hit],
saved.duration
)
end
msgs <<
<<~MSG
Total time spent: #{total_spent.duration}
Total time saved: #{total_saved.duration}
Total time wasted: #{total_miss.duration}
MSG
log :info, msgs.join("\n")
end
private
def cache
@cache ||= Cache.new
end
def tables_cache
@tables_cache ||= {}
end
def disable_referential_integrity
connection = ActiveRecord::Base.connection
return yield unless connection.respond_to?(:disable_referential_integrity)
connection.disable_referential_integrity { yield }
end
end
self.reporting_enabled = ENV["ANYFIXTURE_REPORT"] == "1"
end
end
|