Skip to content

Commit

Permalink
Next Version: v1.1.0 (#5)
Browse files Browse the repository at this point in the history
- add timer support
- tidy-up
- bump version to v1.1.0
  • Loading branch information
mblumtritt authored May 29, 2024
1 parent a409e33 commit d8bb5e7
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 130 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ File.open('test.txt', 'w') do |file|
file.puts(:world!)
end
end

# output will look like
# > IO#<<(?)
# /projects/test.rb:1
Expand All @@ -38,6 +39,7 @@ ImLost.trace_exceptions do
rescue SystemCallError
raise('something went wrong!')
end

# output will look like
# x Errno::EEXIST: File exists @ rb_sysopen - /
# /projects/test.rb:2
Expand All @@ -47,7 +49,7 @@ end
# /projects/test.rb:4
```

When you like to know if and when a code point is reached, `ImLost.here` will help:
When you like to know if a code point is reached, `ImLost.here` will help:

```ruby
ImLost.here
Expand Down
4 changes: 2 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ CLOBBER << 'doc'
YARD::Rake::YardocTask.new(:doc) { _1.stats_options = %w[--list-undoc] }

desc 'Run YARD development server'
task('doc:dev' => :clobber) { exec('yard server --reload') }
task('doc:dev' => :clobber) { exec 'yard server --reload' }

task(:default) { exec('rake --tasks') }
task(:default) { exec 'rake --tasks' }
2 changes: 1 addition & 1 deletion examples/foo.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require_relative '../lib/im-lost'
require 'im-lost'

class Foo
def self.create(value:) = new(value)
Expand Down
29 changes: 29 additions & 0 deletions examples/timer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

puts <<~INFO
This is an example how to use named and anonymous timers.
INFO

require_relative '../lib/im-lost'

puts 'Create a named timer:'
ImLost.timer.create(:first)

puts 'Create an anonymous timer:'
second = ImLost.timer.create

sleep(0.5) # or whatever

puts 'print runtime for named timer:'
ImLost.timer[:first]

puts 'print runtime for anonymous timer:'
ImLost.timer[second]

puts 'delete a named timer'
ImLost.timer.delete(:first)

puts 'delete an anonymous timer'
ImLost.timer.delete(second)
199 changes: 174 additions & 25 deletions lib/im-lost.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

#
# If you have overlooked something again and don't really understand what your
# code is doing. If you have to maintain this application but can't really find
# your way around and certainly can't track down that stupid error. If you feel
Expand Down Expand Up @@ -53,6 +54,12 @@ def output=(value)
raise(ArgumentError, "invalid output device - #{value.inspect}")
end

#
# @return [TimerStore] the timer store used to estimate the runtime of
# your code
#
attr_reader :timer

#
# Enables/disables tracing of method calls.
# This is enabled by default.
Expand All @@ -79,6 +86,7 @@ def trace_calls=(value)
# rescue SystemCallError
# raise('something went wrong!')
# end
#
# # output will look like
# # x Errno::EEXIST: File exists @ rb_sysopen - /
# # /projects/test.rb:2
Expand Down Expand Up @@ -170,15 +178,16 @@ def here(test = true)
# file.puts(:world!)
# end
# end
# output will look like
# > IO#<<(?)
# /projects/test.rb:1
# > IO#write(*)
# /projects/test.rb:1
# > IO#puts(*)
# /projects/test.rb:2
# > IO#write(*)
# /projects/test.rb:2
#
# # output will look like
# # > IO#<<(?)
# # /projects/test.rb:1
# # > IO#write(*)
# # /projects/test.rb:1
# # > IO#puts(*)
# # /projects/test.rb:2
# # > IO#write(*)
# # /projects/test.rb:2
#
# @overload trace(*args)
# @param args [[Object]] one or more objects to be traced
Expand All @@ -187,7 +196,6 @@ def here(test = true)
# @see untrace
# @see untrace_all!
#
#
# @overload trace(*args)
# @param args [[Object]] one or more objects to be traced
# @yieldparam args [Object] the traced object(s)
Expand Down Expand Up @@ -257,7 +265,7 @@ def vars(object)
@trace[traced] = traced if traced
end

protected
private

def as_sig(prefix, info, args)
args = args.join(', ')
Expand All @@ -269,8 +277,6 @@ def as_sig(prefix, info, args)
end
end

private

def _trace(arg)
id = arg.__id__
@trace[id] = id if __id__ != id && @output.__id__ != id
Expand Down Expand Up @@ -312,11 +318,11 @@ def _vars(obj, location)
vars = obj.instance_variables
if vars.empty?
@output.puts(' <no instance variables defined>')
else
@output.puts(' instance variables:')
vars.sort!.each do |name|
@output.puts(" #{name}: #{obj.instance_variable_get(name).inspect}")
end
return obj
end
@output.puts(' instance variables:')
vars.sort!.each do |name|
@output.puts(" #{name}: #{obj.instance_variable_get(name).inspect}")
end
obj
end
Expand All @@ -326,16 +332,150 @@ def _local_vars(binding)
vars = binding.local_variables
if vars.empty?
@output.puts(' <no local variables>')
else
@output.puts(' local variables:')
vars.sort!.each do |name|
@output.puts(" #{name}: #{binding.local_variable_get(name).inspect}")
end
return self
end
@output.puts(' local variables:')
vars.sort!.each do |name|
@output.puts(" #{name}: #{binding.local_variable_get(name).inspect}")
end
self
end
end

#
# A store to create and register timers you can use to estimate the runtime of
# some code.
#
# All timers are identified by an unique ID or a name.
#
# @example Use a named timer
# ImLost.timer.create('my_test')
#
# # ...your code here...
#
# ImLost.timer['my_test']
# # => prints the timer name, this location and runtime so far
#
# # ...more code here...
#
# ImLost.timer['my_test']
# # => prints the timer name, this location and runtime since the timer was created
#
# ImLost.timer.delete('my_test')
# # the timer with name 'my_test' is not longer valid now
#
#
# @example Use an anonymous timer (identified by ID)
# tmr = ImLost.timer.create
#
# # ...your code here...
#
# ImLost.timer[tmr]
# # => prints the timer ID, this location and runtime so far
#
# # ...more code here...
#
# ImLost.timer[tmr]
# # => prints the timer ID, this location and runtime since the timer was created
#
# ImLost.timer.delete(tmr)
# # the timer with the ID `tmr` is not longer valid now
#
# @see ImLost.timer
#
class TimerStore
if defined?(Process::CLOCK_MONOTONIC)
# @return [Float] current time
def self.now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
else
# @return [Float] current time
def self.now = ::Time.now
end

# @attribute [r] count
# @return [Integer] the number of registered timers
def count = ids.size

# @attribute [r] empty?
# @return [Boolean] wheter the timer store is empty or not
def empty? = ids.empty?

# @attribute [r] ids
# @return [Array<Integer>] IDs of all registered timers
def ids = (@ll.keys.keep_if { _1.is_a?(Integer) })

#
# Create and register a new named or anonymous timer.
# It print the ID or name of the created timer and includes the location.
#
# @param name [#to_s] optional timer name
# @return [Integer] timer ID
#
def create(name = nil)
timer = []
@ll[id = timer.__id__] = timer
name ? @ll[name = name.to_s] = timer : name = id
@cb[name, Kernel.caller_locations(1, 1)[0]]
timer << name << self.class.now
id
end

#
# Delete and unregister timers.
#
# @param id_or_names [Array<Integer, #to_s>] the IDs or the names
# @return [nil]
#
def delete(*id_or_names)
id_or_names.flatten.each do |id|
if id.is_a?(Integer)
del = @ll.delete(id)
@ll.delete(del[0]) if del
else
del = @ll.delete(id.to_s)
@ll.delete(del.__id__) if del
end
end
nil
end

#
# Print the ID or name and the runtime since timer was created.
# It includes the location.
#
# @param id_or_name [Integer, #to_s] the identifier or the name of the timer
# @return [Integer] timer ID
# @raise [ArgumentError] when the given id or name is not a registered timer
# identifier or name
#
def [](id_or_name)
time = self.class.now
timer = @ll[id_or_name.is_a?(Integer) ? id_or_name : id_or_name.to_s]
raise(ArgumentError, "not a timer - #{id_or_name.inspect}") unless timer
@cb[timer[0], Kernel.caller_locations(1, 1)[0], time - timer[1]]
timer.__id__
end

#
# Print the ID or name and the runtime of all active timers.
# It includes the location.
#
# @return [nil]
#
def all
now = self.class.now
loc = Kernel.caller_locations(1, 1)[0]
@ll.values.uniq.reverse_each { |name, start| @cb[name, loc, now - start] }
nil
end

# @!visibility private
def initialize(&block)
@cb = block
@ll = {}
end
end

ARG_SIG = { rest: '*', keyrest: '**', block: '&' }.compare_by_identity.freeze
NO_NAME = { :* => 1, :** => 1, :& => 1 }.compare_by_identity.freeze
private_constant :ARG_SIG, :NO_NAME
Expand All @@ -344,6 +484,15 @@ def _local_vars(binding)
@caller_locations = true
@output = $stderr.respond_to?(:puts) ? $stderr : STDERR

@timer =
TimerStore.new do |title, location, time|
@output.puts(
"T #{title}: #{time ? "#{time} sec." : 'created'}",
" #{location.path}:#{location.lineno}"
)
end
TimerStore.private_class_method(:new)

@trace_calls = [
TracePoint.new(:c_call) do |tp|
next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__
Expand Down Expand Up @@ -383,7 +532,7 @@ def _local_vars(binding)
'<',
tp,
tp.parameters.map do |kind, name|
next name if %i[* ** &].include?(name)
next name if NO_NAME.key?(name)
"#{ARG_SIG[kind]}#{ctx.local_variable_get(name).inspect}"
end
)
Expand All @@ -392,7 +541,7 @@ def _local_vars(binding)
end
]

supported = RUBY_VERSION >= '3.3.0' ? %i[raise rescue] : %i[raise]
supported = RUBY_VERSION.to_f < 3.3 ? %i[raise] : %i[raise rescue]
@trace_exceptions =
TracePoint.new(*supported) do |tp|
ex = tp.raised_exception.inspect
Expand Down
2 changes: 1 addition & 1 deletion lib/im-lost/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module ImLost
# The version number of the gem.
VERSION = '1.0.2'
VERSION = '1.1.0'
end
10 changes: 7 additions & 3 deletions spec/helper.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# frozen_string_literal: true

require 'stringio'
require_relative '../lib/im-lost'

$stdout.sync = $stderr.sync = $VERBOSE = true

RSpec.configure(&:disable_monkey_patching!)

require 'stringio'

RE_FLOAT = '[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?'

require_relative '../lib/im-lost'
Loading

0 comments on commit d8bb5e7

Please sign in to comment.