cosell 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/History.txt +3 -0
- data/README.rdoc +190 -0
- data/Rakefile +65 -0
- data/cosell.gemspec +65 -0
- data/example/basic_example.rb +99 -0
- data/example/cat_whisperer.rb +87 -0
- data/lib/cosell.rb +48 -0
- data/lib/cosell/announcer.rb +236 -0
- data/lib/cosell/monkey.rb +32 -0
- data/spec/cosell_spec.rb +179 -0
- data/spec/spec_helper.rb +14 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- metadata +126 -0
data/lib/cosell.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Cosell
|
2
|
+
|
3
|
+
# :stopdoc:
|
4
|
+
VERSION = '0.0.2'
|
5
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
6
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
7
|
+
|
8
|
+
# Returns the version string for the library.
|
9
|
+
#
|
10
|
+
def self.version
|
11
|
+
VERSION
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the library path for the module. If any arguments are given,
|
15
|
+
# they will be joined to the end of the libray path using
|
16
|
+
# <tt>File.join</tt>.
|
17
|
+
#
|
18
|
+
def self.libpath( *args )
|
19
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the lpath for the module. If any arguments are given,
|
23
|
+
# they will be joined to the end of the path using
|
24
|
+
# <tt>File.join</tt>.
|
25
|
+
#
|
26
|
+
def self.path( *args )
|
27
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Utility method used to require all files ending in .rb that lie in the
|
31
|
+
# directory below this file that has the same name as the filename passed
|
32
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
33
|
+
# the _filename_ does not have to be equivalent to the directory.
|
34
|
+
#
|
35
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
36
|
+
dir ||= ::File.basename(fname, '.*')
|
37
|
+
search_me = ::File.expand_path(
|
38
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
39
|
+
|
40
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
41
|
+
end
|
42
|
+
# :startdoc:
|
43
|
+
|
44
|
+
end # module Cosell
|
45
|
+
|
46
|
+
Cosell.require_all_libs_relative_to(__FILE__)
|
47
|
+
|
48
|
+
# EOF
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Cosell
|
4
|
+
|
5
|
+
def initialize *args
|
6
|
+
initialize_cosell!
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# ANNOUNCEMENTS QUEUE
|
13
|
+
#
|
14
|
+
#
|
15
|
+
|
16
|
+
# Place all announcments in a queue, and make announcements in a background thread.
|
17
|
+
#
|
18
|
+
# Arguments:
|
19
|
+
# :logger => a logger. Where to log exceptions and warnings.
|
20
|
+
# This argument is mandatory -- it is too hard to debug exceptions in announcement
|
21
|
+
# handler code without a logger. If you _really_ want your code to fail silently,
|
22
|
+
# you will have to create a logger on /dev/null.
|
23
|
+
# :sleep_time => how long to sleep (in seconds) after making a batch of announchements
|
24
|
+
# optional arg, default: 0.01
|
25
|
+
# :announcements_per_cycle => how many announcements to make before sleeping for sleep_time
|
26
|
+
# optional arg, default: 25
|
27
|
+
#
|
28
|
+
# WARNING: If you do not pass in a logger, announcement code will fail silently (the queue
|
29
|
+
# is in a background thread).
|
30
|
+
#
|
31
|
+
# Note: at the moment, this method may only be called once, and cannot be undone. There is
|
32
|
+
# no way to interrupt the thread.
|
33
|
+
|
34
|
+
def queue_announcements!(opts = {})
|
35
|
+
|
36
|
+
self.initialize_cosell_if_needed
|
37
|
+
|
38
|
+
# The logger in mandatory
|
39
|
+
if opts[:logger]
|
40
|
+
self.queue_logger = opts[:logger]
|
41
|
+
else
|
42
|
+
raise "Cosell error: You have to provide a logger, otherwise failures in announcement handler code are to hard to debug"
|
43
|
+
end
|
44
|
+
|
45
|
+
# kill off the last queue first
|
46
|
+
if self.announcements_thread
|
47
|
+
kill_queue!
|
48
|
+
sleep 0.01
|
49
|
+
queue_announcements! opts
|
50
|
+
end
|
51
|
+
|
52
|
+
self.should_queue_announcements = true
|
53
|
+
@__announcements_queue ||= Queue.new
|
54
|
+
|
55
|
+
how_many_per_cycle = opts[:announcements_per_cycle] || 25
|
56
|
+
cycle_duration = opts[:sleep_time] || 0.01
|
57
|
+
count = 0
|
58
|
+
|
59
|
+
self.announcements_thread = Thread.new do
|
60
|
+
loop do
|
61
|
+
if queue_killed?
|
62
|
+
self.kill_announcement_queue = false
|
63
|
+
self.announcements_thread = nil
|
64
|
+
log "Announcement queue killed with #{self.announcements_queue.size} announcements still queued", :info
|
65
|
+
break
|
66
|
+
else
|
67
|
+
begin
|
68
|
+
self.announce_now! self.announcements_queue.pop
|
69
|
+
count += 1
|
70
|
+
if (count%how_many_per_cycle).eql?(0)
|
71
|
+
log "Announcement queue finished batch of #{how_many_per_cycle}, sleeping for #{cycle_duration} sec", :debug
|
72
|
+
count = 0
|
73
|
+
sleep cycle_duration
|
74
|
+
end
|
75
|
+
rescue Exception => x
|
76
|
+
log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
#
|
86
|
+
# SUBSCRIBE/MAKE ANNOUNCEMENTS
|
87
|
+
#
|
88
|
+
#
|
89
|
+
|
90
|
+
# Pass in an anouncement class (or array of announcement classes), along with a block defining the
|
91
|
+
# action to be taken when an announcment of one of the specified classes is announced by this announcer.
|
92
|
+
# (see Cossell::Announcer for full explanation)
|
93
|
+
def subscribe *announce_classes, &block
|
94
|
+
|
95
|
+
self.initialize_cosell_if_needed
|
96
|
+
|
97
|
+
Array(announce_classes).each do |announce_class|
|
98
|
+
raise "Can only subscribe to classes. Not a class: #{announce_class}" unless announce_class.is_a?(Class)
|
99
|
+
self.subscriptions[announce_class] ||= []
|
100
|
+
self.subscriptions[announce_class] << lambda(&block)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
alias_method :when_announcing, :subscribe
|
104
|
+
|
105
|
+
# Stop announcing for a given announcement class (or array of classes)
|
106
|
+
def unsubscribe *announce_classes
|
107
|
+
Array(announce_classes).each do |announce_class|
|
108
|
+
self.subscriptions.delete announce_class
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# If queue_announcements? true, puts announcement in a Queue.
|
113
|
+
# Otherwise, calls announce_now!
|
114
|
+
# Queued announcements are announced in a background thread in batches
|
115
|
+
# (see the #initialize method doc for details).
|
116
|
+
def announce announcement
|
117
|
+
if self.queue_announcements?
|
118
|
+
self.announcements_queue << announcement
|
119
|
+
else
|
120
|
+
self.announce_now! announcement
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# First, an announcement is made by calling 'as_announcement' on an_announcement_or_announcement_factory,
|
126
|
+
# and subscribers to the announcement's class are then notified
|
127
|
+
#
|
128
|
+
# subscribers to this announcer will be filtered to those that match to the announcement's class,
|
129
|
+
# and those subscriptions will be 'fired'. Subscribers should use the 'subscribe' method (also
|
130
|
+
# called 'when_announcing') to configure actions to take when a given announcement is made.
|
131
|
+
#
|
132
|
+
# Typically, an announcement is passed in for an_announcement_factory, in
|
133
|
+
# which case as_announcement does nothing but return the announcement. But any class can override
|
134
|
+
# as_announcement to adapt into an anouncement as they see fit.
|
135
|
+
#
|
136
|
+
# (see Cossell::Announcer for full explanation)
|
137
|
+
#
|
138
|
+
def announce_now! an_announcement_or_announcement_factory
|
139
|
+
announcement = an_announcement_or_announcement_factory.as_announcement
|
140
|
+
|
141
|
+
self.subscriptions.each do |subscription_type, subscriptions_for_type |
|
142
|
+
if announcement.is_a?(subscription_type)
|
143
|
+
subscriptions_for_type.each{|subscription| subscription.call(announcement) }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
return announcement
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
#
|
152
|
+
# DEBUG
|
153
|
+
#
|
154
|
+
#
|
155
|
+
|
156
|
+
#
|
157
|
+
# Log a message every time this announcer makes an announcement
|
158
|
+
#
|
159
|
+
# Options:
|
160
|
+
# :on => Which class of announcements to spy on. Default is Object (ie. all announcements)
|
161
|
+
# :logger => The log to log to. Default is a logger on STDOUT
|
162
|
+
# :level => The log level to log with. Default is :info
|
163
|
+
# :preface_with => A message to prepend to all log messages. Default is "Announcement Spy: "
|
164
|
+
def spy!(opts = {})
|
165
|
+
on = opts[:on] || Object
|
166
|
+
logger = opts[:logger] || Logger.new(STDOUT)
|
167
|
+
level = opts[:level] || :info
|
168
|
+
preface = opts[:preface_with] || "Announcement Spy: "
|
169
|
+
self.subscribe(on){|ann| logger.send(level, "#{preface} #{ann.as_announcement_trace}")}
|
170
|
+
end
|
171
|
+
|
172
|
+
# lazy initialization of cosell.
|
173
|
+
# Optional -- calling this will get rid of any subsequent warnings about uninitialized ivs
|
174
|
+
# In most cases not necessary, and should never have an effect except to get rid of some warnings.
|
175
|
+
def initialize_cosell_if_needed
|
176
|
+
self.initialize_cosell! if @__subscriptions.nil?
|
177
|
+
end
|
178
|
+
|
179
|
+
# Will blow away any queue, and reset all state.
|
180
|
+
# Should not be necessary to call this, but left public for testing.
|
181
|
+
def initialize_cosell!
|
182
|
+
# Using pseudo-scoped var names.
|
183
|
+
# Unfortunately cant lazily init these w/out ruby warnings going berzerk in verbose mode,
|
184
|
+
# So explicitly declaring them here.
|
185
|
+
@__queue_announcements ||= false
|
186
|
+
@__announcements_queue ||= nil
|
187
|
+
@__kill_announcement_queue ||= false
|
188
|
+
@__announcements_thread ||= nil
|
189
|
+
@__subscriptions ||= {}
|
190
|
+
@__queue_logger ||= {}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Kill the announcments queue.
|
194
|
+
# This is called automatically if you call queue_announcements!, before starting the next
|
195
|
+
# announcments thread, so it's optional. A way of stopping announcments.
|
196
|
+
def kill_queue!
|
197
|
+
@__kill_announcement_queue = true
|
198
|
+
end
|
199
|
+
|
200
|
+
# return whether annoucements are queued or sent out immediately when the #announce method is called.
|
201
|
+
def queue_announcements?
|
202
|
+
return @__queue_announcements.eql?(true)
|
203
|
+
end
|
204
|
+
|
205
|
+
def subscriptions= x; @__subscriptions = x; end
|
206
|
+
def subscriptions; @__subscriptions ||= []; end
|
207
|
+
|
208
|
+
protected
|
209
|
+
|
210
|
+
#:stopdoc:
|
211
|
+
|
212
|
+
def log(msg, level = :info)
|
213
|
+
self.queue_logger.send(level, msg) if self.queue_logger
|
214
|
+
end
|
215
|
+
|
216
|
+
# return whether the queue was killed by kill_queue!
|
217
|
+
def queue_killed?
|
218
|
+
@__kill_announcement_queue.eql?(true)
|
219
|
+
end
|
220
|
+
|
221
|
+
def queue_logger; @__queue_logger; end
|
222
|
+
def queue_logger= x; @__queue_logger = x; end
|
223
|
+
def announcements_queue; @__announcements_queue; end
|
224
|
+
def announcements_queue= x; @__announcements_queue = x; end
|
225
|
+
def announcements_thread; @__announcements_thread; end
|
226
|
+
def announcements_thread= x; @__announcements_thread = x; end
|
227
|
+
def kill_announcement_queue= x; @__kill_announcement_queue = x; end
|
228
|
+
def should_queue_announcements= x; @__queue_announcements = x; end
|
229
|
+
|
230
|
+
#:startdoc:
|
231
|
+
public
|
232
|
+
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#
|
2
|
+
# Cosell is intended to be way for objects to
|
3
|
+
# communicate throughout the Object graph. It is _supposed_ to be
|
4
|
+
# pervasive. As such, it has a few top-level methods that all objects inherit.
|
5
|
+
#
|
6
|
+
class Object
|
7
|
+
|
8
|
+
# When an object (or class) is announced, :as_announcement is called, the result of which
|
9
|
+
# becomes the announcement. By default just returns self, but can be overridden if appropriate.
|
10
|
+
# By default, simply return self.
|
11
|
+
def as_announcement
|
12
|
+
return self
|
13
|
+
end
|
14
|
+
|
15
|
+
# When cosell is configured to "spy!", the result of announement.as_announcement_trace is what
|
16
|
+
# is sent to the spy log. By default just calls 'to_s'.
|
17
|
+
def as_announcement_trace
|
18
|
+
self.to_s rescue "(Warning: could not create announcement trace)"
|
19
|
+
end
|
20
|
+
|
21
|
+
# When a class is used as an announcment, an empty new instance is created using #allocate.
|
22
|
+
# Will raise an exception for those rare classes that cannot #allocate a new instance.
|
23
|
+
def self.as_announcement
|
24
|
+
new_inst = self.allocate rescue nil
|
25
|
+
raise "Cannot create an announcement out of #{self}. Please implement 'as_announcement' as a class method of #{self}." if new_inst.nil?
|
26
|
+
new_inst
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
|
data/spec/cosell_spec.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
#
|
4
|
+
# A few announcments
|
5
|
+
#
|
6
|
+
class AWordFromOurSponsor
|
7
|
+
attr_accessor :word
|
8
|
+
end
|
9
|
+
class KnockOut; end
|
10
|
+
class TKO < KnockOut; end
|
11
|
+
|
12
|
+
#
|
13
|
+
# A class whose objects can act as announcers
|
14
|
+
#
|
15
|
+
class AnyOldClass; include Cosell; end
|
16
|
+
|
17
|
+
#
|
18
|
+
# The tests
|
19
|
+
#
|
20
|
+
describe Cosell do
|
21
|
+
|
22
|
+
before(:each) do
|
23
|
+
@announcer = AnyOldClass.new
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should instantiate announcement instance from class if needed" do
|
27
|
+
@announcer.announce(AWordFromOurSponsor).class.should be_eql(AWordFromOurSponsor)
|
28
|
+
@announcer.announce(AWordFromOurSponsor.new).class.should be_eql(AWordFromOurSponsor)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should execute block specified by subscription" do
|
32
|
+
|
33
|
+
#@announcer.spy!
|
34
|
+
|
35
|
+
# After subscribing to KnockOut, make sure it fires whenever
|
36
|
+
# KnockOut or it's subclass TKO are announced.
|
37
|
+
|
38
|
+
what_was_announced = nil
|
39
|
+
@announcer.when_announcing(AWordFromOurSponsor, KnockOut) { |ann| what_was_announced = ann }
|
40
|
+
|
41
|
+
what_was_announced = nil
|
42
|
+
@announcer.announce KnockOut
|
43
|
+
what_was_announced.should_not be_nil
|
44
|
+
what_was_announced.class.should be_eql KnockOut
|
45
|
+
|
46
|
+
what_was_announced = nil
|
47
|
+
@announcer.announce TKO
|
48
|
+
what_was_announced.should_not be_nil
|
49
|
+
what_was_announced.class.should be_eql TKO
|
50
|
+
|
51
|
+
|
52
|
+
# Do the same thing as above, but announce instances (test above used the class as the announcement)
|
53
|
+
# make sure if an announcement instance is announced, that the exact instance is what is announced
|
54
|
+
|
55
|
+
what_was_announced = nil
|
56
|
+
announcement = AWordFromOurSponsor.new
|
57
|
+
announcement.word = 'the'
|
58
|
+
@announcer.announce announcement
|
59
|
+
what_was_announced.should_not be_nil
|
60
|
+
what_was_announced.class.should be_eql AWordFromOurSponsor
|
61
|
+
what_was_announced.word.should be_eql('the')
|
62
|
+
|
63
|
+
what_was_announced = nil
|
64
|
+
@announcer.announce TKO.new
|
65
|
+
what_was_announced.should_not be_nil
|
66
|
+
what_was_announced.class.should be_eql TKO
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should take actions only on announcements of events for which there is a subscription" do
|
71
|
+
# Make sure the subscription block fires when an AWordFromOurSponsor is
|
72
|
+
# announced, setting what_was_announced to the announcement)
|
73
|
+
what_was_announced = nil
|
74
|
+
@announcer.when_announcing(KnockOut) { |ann| what_was_announced = ann }
|
75
|
+
|
76
|
+
@announcer.announce AWordFromOurSponsor
|
77
|
+
what_was_announced.should be_nil
|
78
|
+
|
79
|
+
@announcer.announce AWordFromOurSponsor.new # also test announcement instances
|
80
|
+
what_was_announced.should be_nil
|
81
|
+
|
82
|
+
@announcer.announce TKO # subclass of Knockout, should be announced
|
83
|
+
what_was_announced.should_not be_nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should be able to subscribe to set of announcements types" do
|
87
|
+
what_was_announced = nil
|
88
|
+
@announcer.when_announcing(AWordFromOurSponsor, KnockOut) { |ann| what_was_announced = ann }
|
89
|
+
|
90
|
+
what_was_announced = nil
|
91
|
+
@announcer.announce AWordFromOurSponsor
|
92
|
+
what_was_announced.should_not be_nil
|
93
|
+
|
94
|
+
what_was_announced = nil
|
95
|
+
@announcer.announce KnockOut
|
96
|
+
what_was_announced.should_not be_nil
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should not take actions after unsubscribing" do
|
100
|
+
what_was_announced = nil
|
101
|
+
@announcer.when_announcing(AWordFromOurSponsor, KnockOut) { |ann| what_was_announced = ann }
|
102
|
+
@announcer.announce AWordFromOurSponsor
|
103
|
+
what_was_announced.should_not be_nil
|
104
|
+
|
105
|
+
@announcer.unsubscribe(AWordFromOurSponsor)
|
106
|
+
what_was_announced = nil
|
107
|
+
@announcer.announce AWordFromOurSponsor
|
108
|
+
what_was_announced.should be_nil
|
109
|
+
@announcer.announce KnockOut
|
110
|
+
what_was_announced.should_not be_nil
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should be able to queue announcements" do
|
114
|
+
what_was_announced = nil
|
115
|
+
count = 0
|
116
|
+
sleep_time = 0.1
|
117
|
+
how_many_each_cycle = 77
|
118
|
+
@announcer.queue_announcements!(:sleep_time => sleep_time,
|
119
|
+
:logger => Logger.new(STDOUT),
|
120
|
+
:announcements_per_cycle => how_many_each_cycle)
|
121
|
+
@announcer.when_announcing(AWordFromOurSponsor) { |ann| count += 1 }
|
122
|
+
|
123
|
+
little_bench("time to queue 10_000 announcements"){10_000.times {@announcer.announce AWordFromOurSponsor}}
|
124
|
+
|
125
|
+
# @announcer.spy! #dbg
|
126
|
+
|
127
|
+
# Allow announcer thread to do a few batches of announcements, checking the
|
128
|
+
# count after each batch. Since we may get to this part of the thread after
|
129
|
+
# the announcer has already made a few announcements, use the count at
|
130
|
+
# this moment as the starting_count
|
131
|
+
start_count = count
|
132
|
+
#puts "-------start count: #{count}" # dbg
|
133
|
+
|
134
|
+
sleep sleep_time + 0.01
|
135
|
+
#puts "-------count: #{count}" # dbg
|
136
|
+
count.should be_eql(start_count + 1*how_many_each_cycle)
|
137
|
+
|
138
|
+
sleep sleep_time
|
139
|
+
#puts "-------count: #{count}" # dbg
|
140
|
+
count.should be_eql(start_count + 2*how_many_each_cycle)
|
141
|
+
|
142
|
+
sleep sleep_time
|
143
|
+
#puts "-------count: #{count}" # dbg
|
144
|
+
count.should be_eql(start_count + 3*how_many_each_cycle)
|
145
|
+
|
146
|
+
# See if killing the queue stops announcments that where queued
|
147
|
+
@announcer.kill_queue!
|
148
|
+
count_after_queue_stopped = count
|
149
|
+
#puts "-------count after stopping: #{count}" # dbg
|
150
|
+
sleep sleep_time * 2
|
151
|
+
count.should be_eql(count_after_queue_stopped)
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
# it "should suppress announcements during suppress_announcements block" do
|
156
|
+
# # TODO: support for this idiom:
|
157
|
+
# notifier.suppress_announcements_during {
|
158
|
+
# }
|
159
|
+
# and
|
160
|
+
# notifier.suppress_announcements(EventType,
|
161
|
+
# :during => lambda { "some operation" },
|
162
|
+
# :send_unique_events_when_done => true)
|
163
|
+
# and
|
164
|
+
# notifier.suppress_announcements(EventType,
|
165
|
+
# :during => lambda { "some operation" },
|
166
|
+
# :send_all_events_when_done => true)
|
167
|
+
# end
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
def little_bench(msg, &block)
|
172
|
+
start = Time.now
|
173
|
+
result = block.call
|
174
|
+
puts "#{msg}: #{Time.now - start} sec"
|
175
|
+
return result
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# EOF
|