Feature #3917
Updated by ko1 (Koichi Sasada) over 12 years ago
=begin I propose to introduce Kernel#called_from() which is similar to caller() but much faster than it. Background ---------- There are some cases to want to know from where current method is called. In this case, Kernel#caller() is used. But Kernel#caller() has performance issues for these cases. * caller() retrieves entire stack frame. It is too heavy. * caller() returns an array of "filename:linenum in `method'" string. User must parse it and retrieve filename and linenum by rexp. It is also very heavy weight task. Therefore I propose Kernel#called_from() which is very light weight compared to caller(). A certain benchmark shows that called_from() is more than 20 times faster tan caller(). 現在のメソッドがどこから呼び出されたかを知りたい場合がときどきある。 こういう場合は通常 Kernel#caller() が使われる。 しかし Kernel#caller() は、こういった用途ではパフォーマンスが非常に悪い。 * caller() はスタックフレームをすべて取り出す。これは非常に重い操作。 * caller() は "ファイル名:行番号 in `メソッド名'" という文字列の配列を返す。 ユーザは正規表現を使ってこの文字列をわざわざパースしなければならない。 これも重い操作。 そのため、Kernel#called_from() を追加することを提案する。 このメソッドは caller() と比べて非常に動作が軽く、ベンチマークでは called_from() は caller() と比べて20倍以上高速。 Spec ----- call-seq: called_from(start=1) -> array or nil Returns file name, line number, and method name of the stack. The optional _start_ parameter represents the number of stack entries to skip. Returns +nil+ if _start_ is greater than the size of current execution stack. Raises ArgumentError if _start_ is negative value. Example code ------------ # example.rb 1: def f1() 2: f2() 3: end 4: def f2() 5: f3() 6: end 7: def f3() 8: p called_from() #=> ["example.rb", 5, "f2"] 9: p called_from(0) #=> ["example.rb", 9, "f3"] 10: p called_from(1) #=> ["example.rb", 5, "f2"] 11: p called_from(2) #=> ["example.rb", 2, "f1"] 12: p called_from(3) #=> ["example.rb", 15, "<main>"] 13: p called_from(4) #=> nil 14: end 15: f1() Use Case -------- Case 1: logging method def log_info(message) filename, linenum, _ = called_from() # !!! @logger.info "#{filename}:#{linenum}: #{message}" end Case 2: debug print def debug(message) filename, linenum, _ = called_from() # !!! $stderr.puts "*** DEBUG: #{filename}:#{linenum}: #{message}" end Case 3: deprecation message def send(*args) filename, linenum, _ = called_from() # !!! msg = "`send()' is deprecated. use `__send__()' instead." msg << " (file: #{filename}, line: #{linenum})" $stderr.puts "*** warning: #{msg}" __send__(*args) end Case 4: ActiveSupport::Testing::Pending module ActiveSupport::Testing::Peding def pending(description = "", &block) : #caller[0] =~ (/(.*):(.*):in `(.*)'/) # original #@@pending_cases << "#{$3} at #{$1}, line #{$2}" # original #print "P" # original filenemae, linenum, method = called_from() # !!! @@pending_cases << "#{method} at #{filename}, line #{linenum}" print "P" : end end Case 5: activesupport/lib/active_support/core_ext/module/delegation.rb class Module def delegate(*methods) : #file, line = caller.first.split(':', 2) # original #line = line.to_i # original file, line, _ = called_from() # !!! : module_eval(<<-EOS, file, line - 5) : end end Case 6: caching helper for template system def cache_with(key) data, created_at = @_cache_store.get(key) filename, = called_from() # !!! ## if template file is newer than cached data then clear cache. ## (performance is very important in this case.) if created_at < File.mtime(filename) data = nil @_cache_store.del(key) end ## if data.nil? len = @_buf.length yield data = @_buf[len..-1] @_cache_store.set(key, data) else @_buf << data end nil end ## in template file <% cache_with("orders/#{@order.id}") do %> <p>Order ID: <%=h @order.id %></p> <p>Customer: <%=h @order.customer.name %></p> <% end %> Benchmark --------- Attached benchmark shows that called_from() is much faster than caller(). This is very important for logging or template timestamp check. $ ./ruby -s bench.rb -N=100000 user system total real caller()[0] 1.890000 0.010000 1.900000 ( 1.941812) caller()[0] (retrieve) 2.190000 0.010000 2.200000 ( 2.225966) called_from() 0.100000 0.000000 0.100000 ( 0.102810) called_from() (retrieve) 0.100000 0.000000 0.100000 ( 0.102133) Another Solutions ----------------- Adding new gobal function may be refused. The followings are another solutions instead of new global function. * Extend caller() to take 'count' parameter. For example: start = 1 count = 1 caller(start, count) #=> ["filename:linenum in `method'"] * Extend caller() to take 'conbine' flag. For example: start = 1 count = nil conbine = false caller(start, count, conbine) #=> [["filename", linenum, "method"], # ["filename", linenum, "method"], # .... ] * Add new standard library 'called_from.so' instead of Kernel#called_from(). 新しいグローバル関数を導入するのは拒絶される可能性が高い。 その場合は、caller()を拡張してcalled_from()相当のことができるように してもらえるとうれしい。 あるいは Kernel#called_from() ではなくても called_from.so を標準添付 する方針でもいい。 Note ---- * I tried to implement the above solutions, but failed because vm_backtrace_each() seems to search stack frames in the reverse order of what called_from() requires. * I can implement called_from() as user library in Ruby 1.8. http://rubygems.org/gems/called_from It is allowed to access to stack frame in Ruby 1.8, but no in 1.9. This is why I submit this propose. * 実は上記のanother solutionsを実装しようとしたが、called_from() では 直近のスタックフレームから辿りたいのに対し、vm_backtrace_each() は 逆の順番で辿ることしかできないようなので、実装を諦めた。 * Ruby 1.8 では拡張モジュールからスタックフレームにアクセスできるので ライブラリとして実装した。 http://rubygems.org/gems/called_from けど1.9ではスタックフレームへのアクセスができないので、ライブラリが 作れない。そのため今回このような提案をしてみた。 =end