2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2017-10-18 09:10:12 +08:00
# see https://samsaffron.com/archive/2017/10/18/fastest-way-to-profile-a-method-in-ruby
class MethodProfiler
2018-02-28 07:45:11 +08:00
def self . patch ( klass , methods , name , no_recurse : false )
2017-10-18 09:10:12 +08:00
patches =
methods
. map do | method_name |
2018-02-28 07:45:11 +08:00
recurse_protection = " "
recurse_protection = << ~ RUBY if no_recurse
return #{method_name}__mp_unpatched(*args, &blk) if @mp_recurse_protect_#{method_name}
@mp_recurse_protect_ #{method_name} = true
RUBY
2017-10-18 09:10:12 +08:00
<< ~ RUBY
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-28 07:45:11 +08:00
#{recurse_protection}
2017-10-18 09:10:12 +08: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-28 07:45:11 +08:00
#{"@mp_recurse_protect_#{method_name} = false" if no_recurse}
2017-10-18 09:10:12 +08:00
end
end
end
RUBY
end
. join ( " \n " )
klass . class_eval patches
end
2021-03-19 15:48:30 +08:00
def self . patch_with_debug_sql ( klass , methods , name , no_recurse : false )
patches =
methods
. map do | method_name |
recurse_protection = " "
recurse_protection = << ~ RUBY if no_recurse
return #{method_name}__mp_unpatched_debug_sql(*args, &blk) if @mp_recurse_protect_#{method_name}
@mp_recurse_protect_ #{method_name} = true
RUBY
<< ~ RUBY
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
end
. join ( " \n " )
klass . class_eval patches
end
2017-11-28 13:47:20 +08:00
def self . transfer
result = Thread . current [ :_method_profiler ]
Thread . current [ :_method_profiler ] = nil
result
end
def self . start ( transfer = nil )
2023-08-02 10:46:37 +08:00
Thread . current [ :_method_profiler ] = transfer ||
2024-11-29 00:25:48 +08:00
{
__start : Process . clock_gettime ( Process :: CLOCK_MONOTONIC ) ,
__start_gc_heap_live_slots : GC . stat [ :heap_live_slots ] ,
}
2017-10-18 09:10:12 +08:00
end
2018-01-19 05:26:18 +08:00
def self . clear
Thread . current [ :_method_profiler ] = nil
end
2017-10-18 09:10:12 +08:00
def self . stop
finish = Process . clock_gettime ( Process :: CLOCK_MONOTONIC )
2023-08-02 09:16:32 +08:00
2017-10-18 09:10:12 +08:00
if data = Thread . current [ :_method_profiler ]
Thread . current [ :_method_profiler ] = nil
start = data . delete ( :__start )
data [ :total_duration ] = finish - start
end
2023-08-02 09:16:32 +08:00
2017-10-18 09:10:12 +08:00
data
end
2019-03-05 19:19:11 +08:00
2021-03-19 15:48:30 +08: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 )
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. " ,
)
@@instrumentation_debug_sql_filter_transactions = filter_transactions
@@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
end
2019-03-05 19:19:11 +08:00
def self . ensure_discourse_instrumentation!
@@instrumentation_setup || =
begin
MethodProfiler . patch (
PG :: Connection ,
% i [ exec async_exec exec_prepared send_query_prepared query exec_params ] ,
:sql ,
)
2023-01-09 20:10:19 +08:00
2019-03-05 19:19:11 +08:00
MethodProfiler . patch ( Redis :: Client , % i [ call call_pipeline ] , :redis )
MethodProfiler . patch ( Net :: HTTP , [ :request ] , :net , no_recurse : true )
2023-01-09 20:10:19 +08:00
2019-03-05 19:19:11 +08:00
MethodProfiler . patch ( Excon :: Connection , [ :request ] , :net )
2023-01-09 20:10:19 +08:00
true
end
end
2017-10-18 09:10:12 +08:00
end