2019-05-02 17:17:27 -05:00
# frozen_string_literal: true
2017-10-17 20:10:12 -05:00
# see https://samsaffron.com/archive/2017/10/18/fastest-way-to-profile-a-method-in-ruby
class MethodProfiler
2018-02-27 17:45:11 -06:00
def self . patch ( klass , methods , name , no_recurse : false )
2023-01-09 06:10:19 -06:00
patches =
methods
. map do | method_name |
recurse_protection = " "
recurse_protection = << ~ RUBY if no_recurse
2018-02-27 17:45:11 -06:00
return #{method_name}__mp_unpatched(*args, &blk) if @mp_recurse_protect_#{method_name}
@mp_recurse_protect_ #{method_name} = true
RUBY
2023-01-09 06:10:19 -06:00
<< ~ RUBY
2017-10-17 20:10:12 -05:00
unless defined? ( #{method_name}__mp_unpatched)
alias_method : #{method_name}__mp_unpatched, :#{method_name}
def #{method_name}(*args, &blk)
unless prof = Thread . current [ :_method_profiler ]
return #{method_name}__mp_unpatched(*args, &blk)
end
2018-02-27 17:45:11 -06:00
#{recurse_protection}
2017-10-17 20:10:12 -05:00
begin
start = Process . clock_gettime ( Process :: CLOCK_MONOTONIC )
#{method_name}__mp_unpatched(*args, &blk)
ensure
data = ( prof [ : #{name}] ||= {duration: 0.0, calls: 0})
data [ :duration ] += Process . clock_gettime ( Process :: CLOCK_MONOTONIC ) - start
data [ :calls ] += 1
2018-02-27 17:45:11 -06:00
#{"@mp_recurse_protect_#{method_name} = false" if no_recurse}
2017-10-17 20:10:12 -05:00
end
end
end
RUBY
2023-01-09 06:10:19 -06:00
end
. join ( " \n " )
2017-10-17 20:10:12 -05:00
klass . class_eval patches
end
2021-03-19 02:48:30 -05:00
def self . patch_with_debug_sql ( klass , methods , name , no_recurse : false )
2023-01-09 06:10:19 -06:00
patches =
methods
. map do | method_name |
recurse_protection = " "
recurse_protection = << ~ RUBY if no_recurse
2021-03-19 02:48:30 -05:00
return #{method_name}__mp_unpatched_debug_sql(*args, &blk) if @mp_recurse_protect_#{method_name}
@mp_recurse_protect_ #{method_name} = true
RUBY
2023-01-09 06:10:19 -06:00
<< ~ RUBY
2021-03-19 02:48:30 -05:00
unless defined? ( #{method_name}__mp_unpatched_debug_sql)
alias_method : #{method_name}__mp_unpatched_debug_sql, :#{method_name}
def #{method_name}(*args, &blk)
#{recurse_protection}
query = args [ 0 ]
should_filter = #{@@instrumentation_debug_sql_filter_transactions} &&
( query == " COMMIT " || query == " BEGIN " || query == " ROLLBACK " )
if ! should_filter
STDERR . puts " debugsql (sql): " + query
end
begin
start = Process . clock_gettime ( Process :: CLOCK_MONOTONIC )
#{method_name}__mp_unpatched_debug_sql(*args, &blk)
ensure
duration = Process . clock_gettime ( Process :: CLOCK_MONOTONIC ) - start
if ! should_filter
STDERR . puts " debugsql (sec): " + duration . round ( 3 ) . to_s
end
#{"@mp_recurse_protect_#{method_name} = false" if no_recurse}
end
end
end
RUBY
2023-01-09 06:10:19 -06:00
end
. join ( " \n " )
2021-03-19 02:48:30 -05:00
klass . class_eval patches
end
2017-11-27 23:47:20 -06:00
def self . transfer
result = Thread . current [ :_method_profiler ]
Thread . current [ :_method_profiler ] = nil
result
end
def self . start ( transfer = nil )
2023-08-01 21:46:37 -05:00
Thread . current [ :_method_profiler ] = transfer ||
{ __start : Process . clock_gettime ( Process :: CLOCK_MONOTONIC ) }
2017-10-17 20:10:12 -05:00
end
2018-01-18 15:26:18 -06:00
def self . clear
Thread . current [ :_method_profiler ] = nil
end
2017-10-17 20:10:12 -05:00
def self . stop
finish = Process . clock_gettime ( Process :: CLOCK_MONOTONIC )
2023-08-01 20:16:32 -05:00
2017-10-17 20:10:12 -05:00
if data = Thread . current [ :_method_profiler ]
Thread . current [ :_method_profiler ] = nil
start = data . delete ( :__start )
data [ :total_duration ] = finish - start
end
2023-08-01 20:16:32 -05:00
2017-10-17 20:10:12 -05:00
data
end
2019-03-05 05:19:11 -06:00
2021-03-19 02:48:30 -05:00
##
# This is almost the same as ensure_discourse_instrumentation! but should not
# be used in production. This logs all SQL queries run and their durations
# between start and stop.
#
# filter_transactions - When true, we do not record timings of transaction
# related commits (BEGIN, COMMIT, ROLLBACK)
def self . output_sql_to_stderr! ( filter_transactions : false )
2023-01-09 06:10:19 -06:00
Rails . logger . warn (
" Stop! This instrumentation is not intended for use in production outside of debugging scenarios. Please be sure you know what you are doing when enabling this instrumentation. " ,
)
2021-03-19 02:48:30 -05:00
@@instrumentation_debug_sql_filter_transactions = filter_transactions
2023-01-09 06:10:19 -06:00
@@instrumentation_setup_debug_sql || =
begin
MethodProfiler . patch_with_debug_sql (
PG :: Connection ,
% i [ exec async_exec exec_prepared send_query_prepared query exec_params ] ,
:sql ,
)
true
end
2021-03-19 02:48:30 -05:00
end
2019-03-05 05:19:11 -06:00
def self . ensure_discourse_instrumentation!
2023-01-09 06:10:19 -06:00
@@instrumentation_setup || =
begin
MethodProfiler . patch (
PG :: Connection ,
% i [ exec async_exec exec_prepared send_query_prepared query exec_params ] ,
:sql ,
)
MethodProfiler . patch ( Redis :: Client , % i [ call call_pipeline ] , :redis )
2019-03-05 05:19:11 -06:00
2023-01-09 06:10:19 -06:00
MethodProfiler . patch ( Net :: HTTP , [ :request ] , :net , no_recurse : true )
MethodProfiler . patch ( Excon :: Connection , [ :request ] , :net )
true
end
end
2017-10-17 20:10:12 -05:00
end