tick_tock 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +9 -0
- data/Gemfile +6 -0
- data/README.md +37 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/lazy_example.rb +21 -0
- data/examples/threaded_example.rb +27 -0
- data/lib/tick_tock.rb +174 -0
- data/lib/tick_tock/card.rb +43 -0
- data/lib/tick_tock/card_logger.rb +58 -0
- data/lib/tick_tock/clock.rb +107 -0
- data/lib/tick_tock/locals.rb +115 -0
- data/lib/tick_tock/punch.rb +30 -0
- data/lib/tick_tock/version.rb +3 -0
- data/tick_tock.gemspec +36 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 78fb0e8694ff38d1b76653ed5d713cd0334368f265e041cb43f4895979effb19
|
4
|
+
data.tar.gz: 6e2de7ccb4128857a521773c9ddf3ea1b67acaacc753be369414a08c7a9d750b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 87a256c0e37232f5aa37e2605aeca0fc1bd8b62c3aa67a24f42235fb11ac30618cb253991bf5896507554bf70fdd805ee50c10400b3215babe70b79964974317
|
7
|
+
data.tar.gz: b15e21a6b9222f418ef40df680b57dfb176e7e59b349faf609216ba1d68ea3d817fba004dacd0f05e94b191090967c3bf9e5789fbb0c6346646dcf534fb48720
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# TickTock
|
2
|
+
|
3
|
+
[](https://travis-ci.org/ms-ati/tick_tock)
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/tick_tock`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
TODO: Delete this and the text above, and describe your gem
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'tick_tock'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install tick_tock
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
TODO: Write usage instructions here
|
28
|
+
|
29
|
+
## Development
|
30
|
+
|
31
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
32
|
+
|
33
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tick_tock.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "tick_tock"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "pp"
|
2
|
+
require "tick_tock"
|
3
|
+
|
4
|
+
lazy_enum = [1, 2, 3].lazy
|
5
|
+
|
6
|
+
slow_proc = ->(n) { sleep(1); n * 2 }
|
7
|
+
|
8
|
+
# See how these can be defined elsewhere, but end up logging hierarchically
|
9
|
+
wrapped_slow_proc =
|
10
|
+
TickTock.tick_tock_proc(slow_proc, subject: ->(n) { "Num #{n}" })
|
11
|
+
|
12
|
+
wrapped_lazy_enum =
|
13
|
+
TickTock.tick_tock_lazy(lazy_enum, subject: "Lazy Enum")
|
14
|
+
|
15
|
+
# Here we both log the block and also hierarchically log what happens inside it:
|
16
|
+
result =
|
17
|
+
TickTock.tick_tock(subject: "Top Level") do
|
18
|
+
wrapped_lazy_enum.map(&wrapped_slow_proc).to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
pp result
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "pp"
|
2
|
+
require "tick_tock"
|
3
|
+
|
4
|
+
inputs = [1, 2, 3]
|
5
|
+
|
6
|
+
slow_proc = ->(n) { sleep(1); n * Thread.current.name.to_i }
|
7
|
+
|
8
|
+
wrapped_slow_proc = TickTock.tick_tock_proc(
|
9
|
+
slow_proc,
|
10
|
+
subject: ->(n) { "Thr #{Thread.current.name}, Num #{n}" }
|
11
|
+
)
|
12
|
+
|
13
|
+
result =
|
14
|
+
TickTock.tick_tock(subject: "Top Level") do
|
15
|
+
thread_proc = TickTock.tick_tock_proc(
|
16
|
+
subject: ->(n) { "Thr #{n}" },
|
17
|
+
save_context: true
|
18
|
+
) do |n|
|
19
|
+
Thread.current.name = n.to_s
|
20
|
+
inputs.map(&wrapped_slow_proc)
|
21
|
+
end
|
22
|
+
|
23
|
+
# In each thread, we pick up logging within the saved context
|
24
|
+
Array.new(3) { |n| Thread.new(n + 1, &thread_proc) }.map(&:value)
|
25
|
+
end
|
26
|
+
|
27
|
+
pp result
|
data/lib/tick_tock.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require "tick_tock/clock"
|
2
|
+
require "tick_tock/version"
|
3
|
+
|
4
|
+
# {TickTock} makes it easy to wrap your Ruby code to measure nested timings and
|
5
|
+
# to log them -- even when the code is asynchronous or lazy, so straight-forward
|
6
|
+
# blocks do not work.
|
7
|
+
#
|
8
|
+
# The TickTock module uses Ruby's
|
9
|
+
# {http://ruby-doc.org/core-2.3.4/Module.html#method-i-module_function
|
10
|
+
# module_function} directive to enable two different usage patterns:
|
11
|
+
# 1. Calling methods directly on the module
|
12
|
+
# 2. Including the module into classes as a mix-in
|
13
|
+
#
|
14
|
+
# == Configuration
|
15
|
+
#
|
16
|
+
# When calling methods directly on the module, the instance of {TickTock::Clock}
|
17
|
+
# configured via {TickTock.clock=} will be used for all calls.
|
18
|
+
#
|
19
|
+
# On the other hand, when including the TickTock module into a class, the class
|
20
|
+
# may choose to override {#clock} to return a specific instance.
|
21
|
+
#
|
22
|
+
# @example 1. Calling TickTock methods directly on the module
|
23
|
+
# TickTock.tick_tock(subject: "a block") do
|
24
|
+
# times_2 = ->(n) { n * 2 }
|
25
|
+
# [1, 2].map(&TickTock.tick_tock_proc(times_2, subject: :to_s.to_proc))
|
26
|
+
# end
|
27
|
+
# # => [2, 4]
|
28
|
+
#
|
29
|
+
# # Logs the following
|
30
|
+
# # I, [2018-02-04T14:22:01.1261 #26] INFO -- : > Started a block
|
31
|
+
# # I, [2018-02-04T14:22:01.1264 #26] INFO -- : >> Started 1
|
32
|
+
# # I, [2018-02-04T14:22:01.1265 #26] INFO -- : << Completed 1 [0.000s]
|
33
|
+
# # I, [2018-02-04T14:22:01.1267 #26] INFO -- : >> Started 2
|
34
|
+
# # I, [2018-02-04T14:22:01.1267 #26] INFO -- : << Completed 2 [0.000s]
|
35
|
+
# # I, [2018-02-04T14:22:01.1268 #26] INFO -- : < Completed a block [0.001s]
|
36
|
+
#
|
37
|
+
# @example 2. Including the module into classes as a mix-in
|
38
|
+
# class Times2
|
39
|
+
# include TickTock
|
40
|
+
#
|
41
|
+
# def call
|
42
|
+
# tick_tock(subject: "a block") do
|
43
|
+
# [1, 2].map(&tick_tock_proc(method(:times_2), subject: :to_s.to_proc))
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# def times_2(n)
|
48
|
+
# n * 2
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# Times2.new.call
|
53
|
+
# # => [2, 4]
|
54
|
+
#
|
55
|
+
# # Logs the same as above
|
56
|
+
#
|
57
|
+
module TickTock
|
58
|
+
class << self
|
59
|
+
# @return [Clock]
|
60
|
+
# the configured global instance of Clock, assigned by calling {.clock=}.
|
61
|
+
attr_accessor :clock
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Clock]
|
65
|
+
# when the TickTock module is included in a class, by default returns the
|
66
|
+
# configured global instance of Clock. Override {#clock} in classes to
|
67
|
+
# return a class-specific instance instead.
|
68
|
+
def clock
|
69
|
+
self.class.clock
|
70
|
+
end
|
71
|
+
|
72
|
+
module_function
|
73
|
+
|
74
|
+
# @!group Basics: Start and stop a timing context
|
75
|
+
|
76
|
+
# Starts a new timing context, returns a "card" representing it.
|
77
|
+
#
|
78
|
+
# @param subject [Object] Description of the subject we are timing
|
79
|
+
# @return [Object] A new card representing the new timing context
|
80
|
+
def tick(subject: nil)
|
81
|
+
clock.tick(subject: subject)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Completes a timing context represented by the given "card".
|
85
|
+
#
|
86
|
+
# @param card [Object] A card representing the timing context to complete
|
87
|
+
# @return [Object] The card after being marked completed
|
88
|
+
def tock(card:)
|
89
|
+
clock.tock(card: card)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @!endgroup
|
93
|
+
|
94
|
+
# @!group Helpers: Wrap an asynchronous construct in a timing context
|
95
|
+
|
96
|
+
# Executes the given block in a timing context using {.tick} and {.tock}.
|
97
|
+
#
|
98
|
+
# @param subject [Object] Description of the subject we are timing
|
99
|
+
# @return [Object] Return value of the given block
|
100
|
+
def tick_tock(subject: nil)
|
101
|
+
card = tick(subject: subject)
|
102
|
+
yield
|
103
|
+
ensure
|
104
|
+
tock(card: card) unless card.nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
# Wraps a
|
108
|
+
# {http://ruby-doc.org/core-2.3.4/Enumerator/Lazy.html lazy enumerator} with
|
109
|
+
# a timing context using *lazy* calls to {.tick} and {.tock}, which will be
|
110
|
+
# called when the enumerator starts and completes enumeration respectively.
|
111
|
+
#
|
112
|
+
# @param lazy_enum_to_wrap [Enumerator::Lazy] Lazy enumerator to wrap
|
113
|
+
# @param subject [Object] Description of the subject
|
114
|
+
# @return [Enumerator::Lazy] Wrapped lazy enumerator
|
115
|
+
def tick_tock_lazy(lazy_enum_to_wrap, subject: nil)
|
116
|
+
shared_state = [nil]
|
117
|
+
|
118
|
+
lazy_tick = proc { shared_state[0] = tick(subject: subject); [] }
|
119
|
+
lazy_tock = proc { shared_state[0] = tock(card: shared_state[0]); [] }
|
120
|
+
|
121
|
+
arr_with_callbacks = [
|
122
|
+
[:dummy].lazy.flat_map(&lazy_tick),
|
123
|
+
lazy_enum_to_wrap.lazy,
|
124
|
+
[:dummy].lazy.flat_map(&lazy_tock)
|
125
|
+
]
|
126
|
+
|
127
|
+
arr_with_callbacks.lazy.flat_map(&:itself)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Wraps a Proc with a timing context using a call to {.tick_tock}. Can
|
131
|
+
# optionally wrap the current nested timing contexts into the Proc, so that
|
132
|
+
# when executed asynchronously it will retain the same context.
|
133
|
+
#
|
134
|
+
# @param callable_to_wrap [Proc, #call]
|
135
|
+
# Callable to wrap, given as a normal parameter.
|
136
|
+
#
|
137
|
+
# @param subject [Object, Proc, #call]
|
138
|
+
# Description of the subject, or optionally a function which, when called on
|
139
|
+
# the args that are actually eventually passed to the final proc, will
|
140
|
+
# generate the actual subject.
|
141
|
+
#
|
142
|
+
# @param save_context [Boolean]
|
143
|
+
# Optionally wrap the currently active timing contexts into the Proc, so
|
144
|
+
# that when executed asynchronously it will retain the same context.
|
145
|
+
#
|
146
|
+
# @param proc_to_wrap [Proc]
|
147
|
+
# Alternatively you can pass the callable as a "block" parameter -- only
|
148
|
+
# used if callable_to_wrap is nil.
|
149
|
+
#
|
150
|
+
# @return [Proc]
|
151
|
+
# A Proc wrapping the given callable in a timing context.
|
152
|
+
def tick_tock_proc(
|
153
|
+
callable_to_wrap = nil,
|
154
|
+
subject: nil,
|
155
|
+
save_context: false,
|
156
|
+
&proc_to_wrap
|
157
|
+
)
|
158
|
+
tt_proc = proc do |*proc_args|
|
159
|
+
# if original subject was a Proc, apply it to args to create the subject
|
160
|
+
subject = subject&.respond_to?(:call) ? subject.call(*proc_args) : subject
|
161
|
+
|
162
|
+
tick_tock(subject: subject) do
|
163
|
+
(callable_to_wrap || proc_to_wrap).call(*proc_args)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
save_context ? Locals.wrap_proc(&tt_proc) : tt_proc
|
168
|
+
end
|
169
|
+
|
170
|
+
# @!endgroup
|
171
|
+
end
|
172
|
+
|
173
|
+
# Assigns a global default clock instance
|
174
|
+
TickTock.clock = TickTock::Clock.default
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "values"
|
2
|
+
|
3
|
+
module TickTock
|
4
|
+
# @api private
|
5
|
+
#
|
6
|
+
# The representation of a "punch card" used by the default {Punch}
|
7
|
+
# implementation
|
8
|
+
#
|
9
|
+
# @!parse
|
10
|
+
# class Card
|
11
|
+
# # Constructor accepting keyword args.
|
12
|
+
# #
|
13
|
+
# # @param subject [Object] Description of subject of this card
|
14
|
+
# # @param parent_card [Card, nil]
|
15
|
+
# # @param time_in [Time, nil]
|
16
|
+
# # @param time_out [Time, nil]
|
17
|
+
# # @return [Card]
|
18
|
+
# def self.with(subject:, parent_card:, time_in:, time_out:); end
|
19
|
+
#
|
20
|
+
# # @param subject [Object] Description of subject of this card
|
21
|
+
# # @param parent_card [Card, nil]
|
22
|
+
# # @param time_in [Time, nil]
|
23
|
+
# # @param time_out [Time, nil]
|
24
|
+
# def initialize(subject, parent_card, time_in, time_out); end
|
25
|
+
#
|
26
|
+
# # @return [Card]
|
27
|
+
# # a copy of this instance with any given values replaced.
|
28
|
+
# #
|
29
|
+
# # @param subject [Object] Description of subject of this card
|
30
|
+
# # @param parent_card [Card, nil]
|
31
|
+
# # @param time_in [Time, nil]
|
32
|
+
# # @param time_out [Time, nil]
|
33
|
+
# def with(subject: nil, parent_card: nil, time_in: nil, time_out: nil)
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @attr_reader subject [Object] Description of subject of this card
|
38
|
+
# @attr_reader parent_card [Card, nil]
|
39
|
+
# @attr_reader time_in [Time, nil]
|
40
|
+
# @attr_reader time_out [Time, nil]
|
41
|
+
class Card < Value.new(:subject, :parent_card, :time_in, :time_out)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "values"
|
3
|
+
|
4
|
+
module TickTock
|
5
|
+
CardLogger = Value.new(
|
6
|
+
:logger,
|
7
|
+
:severity,
|
8
|
+
:secs_decimals,
|
9
|
+
:show_zero_mins,
|
10
|
+
:show_zero_hrs
|
11
|
+
)
|
12
|
+
|
13
|
+
class CardLogger
|
14
|
+
def self.default
|
15
|
+
with(
|
16
|
+
logger: Logger.new($stdout),
|
17
|
+
severity: Logger::INFO,
|
18
|
+
secs_decimals: 3,
|
19
|
+
show_zero_mins: false,
|
20
|
+
show_zero_hrs: false
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(card)
|
25
|
+
logger.add(severity, nil, nil) { format(card) }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def format(card)
|
31
|
+
prefix, verb = card.time_out.nil? ? [">", "Started"] : ["<", "Completed"]
|
32
|
+
suffix = card.time_out && "[#{elapsed(card.time_out - card.time_in)}]"
|
33
|
+
[prefix * depth(card), verb, card.subject, suffix].compact.join(" ")
|
34
|
+
end
|
35
|
+
|
36
|
+
def depth(card, count = 1)
|
37
|
+
card.parent_card.nil? ? count : depth(card.parent_card, count + 1)
|
38
|
+
end
|
39
|
+
|
40
|
+
def elapsed(secs)
|
41
|
+
if secs < 60 && !show_zero_hrs && !show_zero_mins
|
42
|
+
"%.#{secs_decimals}fs" % secs
|
43
|
+
else
|
44
|
+
secs_width = 3 + secs_decimals # because 3 is 2 digits plus a "."
|
45
|
+
|
46
|
+
str = "%02dh:%02dm:%0#{secs_width}.#{secs_decimals}fs" % [
|
47
|
+
secs / 3600, # hours
|
48
|
+
secs / 60 % 60, # minutes
|
49
|
+
secs % 60 # seconds
|
50
|
+
]
|
51
|
+
|
52
|
+
str.sub!("00h:", "") unless show_zero_hrs
|
53
|
+
str.sub!("00m:", "") unless show_zero_hrs || show_zero_mins
|
54
|
+
str
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require "tick_tock/card"
|
2
|
+
require "tick_tock/card_logger"
|
3
|
+
require "tick_tock/locals"
|
4
|
+
require "tick_tock/punch"
|
5
|
+
|
6
|
+
module TickTock
|
7
|
+
# A {TickTock::Clock} can {#tick} and then {#tock}. Inspired by the
|
8
|
+
# {https://en.wikipedia.org/wiki/Time_clock "punch-card" or "bundy" clock}
|
9
|
+
# (where workers carry their own cards to punch in and out): the instances of
|
10
|
+
# Clock are immutable, and the states of timing contexts are kept on "cards".
|
11
|
+
#
|
12
|
+
# == Punch cards
|
13
|
+
#
|
14
|
+
# This class delegates to a {TickTock::Punch} implementation (via {#punch}) in
|
15
|
+
# order to obtain cards, to punch them in, and to punch them out. It does not
|
16
|
+
# depend on any details of those classes. It is therefore possible to replace
|
17
|
+
# the implementation of {Punch} - for example, to provide an alternate time
|
18
|
+
# source, or to use a memory-optimized representation of the punch cards.
|
19
|
+
#
|
20
|
+
# == Hierarchical state
|
21
|
+
#
|
22
|
+
# To represent the hierarchy of currently active punch cards, we use {Locals},
|
23
|
+
# which allows us to get and set state in a manner analogous to thread-local
|
24
|
+
# variables, but which can be captured and accessed across asynchronous
|
25
|
+
# contexts.
|
26
|
+
#
|
27
|
+
# == Logging and any other side-effects
|
28
|
+
#
|
29
|
+
# Callbacks {#on_tick} and {#on_tock} define logging (and any other
|
30
|
+
# {https://en.wikipedia.org/wiki/Side_effect_(computer_science) side-effects})
|
31
|
+
# which should take place on *ticks* and *tocks*.
|
32
|
+
#
|
33
|
+
# @!parse
|
34
|
+
# class Clock
|
35
|
+
# # Constructor accepting keyword args.
|
36
|
+
# #
|
37
|
+
# # @param punch [Punch] Implementation of {Punch} to use
|
38
|
+
# # @param on_tick [Proc, nil] Callback on {#tick}
|
39
|
+
# # @param on_tock [Proc, nil] Callback on {#tock}
|
40
|
+
# # @return [Clock]
|
41
|
+
# def self.with(punch:, on_tick:, on_tock:); end
|
42
|
+
#
|
43
|
+
# # @param punch [Punch] Implementation of {Punch} to use
|
44
|
+
# # @param on_tick [Proc, nil] Callback on {#tick}
|
45
|
+
# # @param on_tock [Proc, nil] Callback on {#tock}
|
46
|
+
# def initialize(punch, on_tick, on_tock); end
|
47
|
+
#
|
48
|
+
# # @return [Clock]
|
49
|
+
# # a copy of this instance with any given values replaced.
|
50
|
+
# #
|
51
|
+
# # @param punch [Punch] Implementation of {Punch} to use
|
52
|
+
# # @param on_tick [Proc, nil] Callback on {#tick}
|
53
|
+
# # @param on_tock [Proc, nil] Callback on {#tock}
|
54
|
+
# def with(punch: nil, on_tick: nil, on_tock: nil); end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @attr_reader punch [Punch] Implementation of {Punch} to use
|
58
|
+
# @attr_reader on_tick [Proc, nil] Callback on {#tick}
|
59
|
+
# @attr_reader on_tock [Proc, nil] Callback on {#tock}
|
60
|
+
class Clock < Value.new(:punch, :on_tick, :on_tock)
|
61
|
+
# @return [Clock] an instance with the default configuration
|
62
|
+
def self.default
|
63
|
+
log_card = CardLogger.default.method(:call)
|
64
|
+
with(punch: Punch.default, on_tick: log_card, on_tock: log_card)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Starts timing a new card, calls any given {#on_tick}, and returns it.
|
68
|
+
#
|
69
|
+
# @param subject [Object] Description of the subject we are timing
|
70
|
+
# @return [Object] A new card representing the new timing context
|
71
|
+
def tick(subject: nil)
|
72
|
+
card = punch.card(subject: subject, parent_card: self.class.current_card)
|
73
|
+
card_in = punch.in(card)
|
74
|
+
self.class.current_card = card_in
|
75
|
+
on_tick&.call(card_in)
|
76
|
+
card_in
|
77
|
+
end
|
78
|
+
|
79
|
+
# Completes timing the given card, calls any given {#on_tock}, and returns
|
80
|
+
# it.
|
81
|
+
#
|
82
|
+
# @param card [Object] A card representing the timing context to complete
|
83
|
+
# @return [Object] The card after being marked completed
|
84
|
+
def tock(card:)
|
85
|
+
card_out = punch.out(card)
|
86
|
+
self.class.current_card = punch.parent_card_of(card_out)
|
87
|
+
on_tock&.call(card_out)
|
88
|
+
card_out
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# @return [Symbol] Key to store and retrieve current punch card in {Locals}
|
94
|
+
CURRENT_CARD = :"__tick_tock/current_card__"
|
95
|
+
private_constant :CURRENT_CARD
|
96
|
+
|
97
|
+
# @return [Punch::Card] The currently active punch card
|
98
|
+
def self.current_card
|
99
|
+
Locals.key?(CURRENT_CARD) ? TickTock::Locals[CURRENT_CARD] : nil
|
100
|
+
end
|
101
|
+
|
102
|
+
# Sets the currently active punch card
|
103
|
+
def self.current_card=(card)
|
104
|
+
Locals[CURRENT_CARD] = card
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module TickTock
|
2
|
+
# {Locals} provides the convenience of Thread-local (or Fiber-local) variables
|
3
|
+
# to asynchronous code. By "asynchronous", we simply mean that a given Proc
|
4
|
+
# may end up executing on a different Thread or Fiber from the one in which
|
5
|
+
# it was created -- or equally -- on the same thread, but at a later time!
|
6
|
+
#
|
7
|
+
# See {.wrap_proc} for an example of how this can be used to capture local
|
8
|
+
# state and wrap it with a Proc for later use, independent of how that state
|
9
|
+
# may change during the intervening period.
|
10
|
+
#
|
11
|
+
# {https://github.com/monix/monix/blob/v3.0.0-M3/monix-execution/shared/src/main/scala/monix/execution/misc/Local.scala
|
12
|
+
# Here's how similar concepts are implemented} in the Monix library in Scala.
|
13
|
+
#
|
14
|
+
module Locals
|
15
|
+
module_function
|
16
|
+
|
17
|
+
# @return [Hash] Default context is an empty hash
|
18
|
+
DEFAULT_CONTEXT = {}.freeze
|
19
|
+
private_constant :DEFAULT_CONTEXT
|
20
|
+
|
21
|
+
# @return [Symbol] Key under which context is stored as a fiber-local var
|
22
|
+
FIBER_LOCAL_KEY = :"__tick_tock/local_context__"
|
23
|
+
private_constant :FIBER_LOCAL_KEY
|
24
|
+
|
25
|
+
# Wraps the current local context into the given proc, so that when it is
|
26
|
+
# run it has access to the same local context as when it was wrapped, even
|
27
|
+
# if it is run in a different Thread, Fiber, or simply at a later time after
|
28
|
+
# the local context was changed.
|
29
|
+
#
|
30
|
+
# @example Wrap current local context into a Proc object
|
31
|
+
# # proc which depends on some local variable
|
32
|
+
# a_proc = proc { "proc sees: " + TickTock::Locals[:foo].to_s }
|
33
|
+
#
|
34
|
+
# # wraps the current state of the locals into the proc
|
35
|
+
# TickTock::Locals[:foo] = :bar
|
36
|
+
# wrapped_proc = TickTock::Locals.wrap_proc(&a_proc)
|
37
|
+
#
|
38
|
+
# # later, the state of the locals change, but the wrapped state does not
|
39
|
+
# TickTock::Locals[:foo] = 42
|
40
|
+
# wrapped_proc.call
|
41
|
+
# #=> "proc sees: bar"
|
42
|
+
#
|
43
|
+
# TickTock::Locals[:foo]
|
44
|
+
# #=> 42
|
45
|
+
#
|
46
|
+
# @param proc_to_wrap [Proc] A proc to wrap with the current local context
|
47
|
+
# @return [Proc] Wrapped version of the given `proc_to_wrap`
|
48
|
+
def wrap_proc(&proc_to_wrap)
|
49
|
+
wrapped_context = context
|
50
|
+
|
51
|
+
proc do |*args|
|
52
|
+
begin
|
53
|
+
saved_context = context
|
54
|
+
self.context = wrapped_context
|
55
|
+
proc_to_wrap.call(*args)
|
56
|
+
ensure
|
57
|
+
self.context = saved_context
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Fetches current value of given `key` in the local context.
|
63
|
+
#
|
64
|
+
# @param key [String, Symbol] Key to fetch
|
65
|
+
# @return [Object] Current value of `key` in local context
|
66
|
+
# @raise [KeyError] Raise if key not found
|
67
|
+
def [](key)
|
68
|
+
context.fetch(key)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Updates the current local context to map *key* to *value*. Works in a
|
72
|
+
# non-mutative way, so that any other references to the old context will be
|
73
|
+
# left unchanged.
|
74
|
+
#
|
75
|
+
# @param key [String, Symbol] Key to set a value for
|
76
|
+
# @param value [Object] Value to set for `key` in local context
|
77
|
+
# @return [Object] The value that was set
|
78
|
+
def []=(key, value)
|
79
|
+
self.context = context.merge(key => value).freeze
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check if the given key is currently set in the local context.
|
83
|
+
#
|
84
|
+
# @param key [String, Symbol] Key to check for
|
85
|
+
# @return [Boolean] Is the key currently set?
|
86
|
+
def key?(key)
|
87
|
+
context.key?(key)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Clears the current local context
|
91
|
+
def clear!
|
92
|
+
self.context = DEFAULT_CONTEXT
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Hash{String, Symbol => Object}]
|
96
|
+
# The current local context represented as a frozen hash.
|
97
|
+
def context
|
98
|
+
if Thread.current.key?(FIBER_LOCAL_KEY)
|
99
|
+
Thread.current[FIBER_LOCAL_KEY]
|
100
|
+
else
|
101
|
+
DEFAULT_CONTEXT
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# @api private
|
106
|
+
#
|
107
|
+
# Sets the local context to the given frozen hash.
|
108
|
+
#
|
109
|
+
# @param hsh [Hash] Frozen hash to set as current local context
|
110
|
+
def context=(hsh)
|
111
|
+
Thread.current[FIBER_LOCAL_KEY] = hsh
|
112
|
+
end
|
113
|
+
private_class_method :context=
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "tick_tock/card"
|
2
|
+
|
3
|
+
module TickTock
|
4
|
+
class Punch < Value.new(:time_now)
|
5
|
+
def self.default
|
6
|
+
with(time_now: Time.method(:now))
|
7
|
+
end
|
8
|
+
|
9
|
+
def card(subject: nil, parent_card: nil)
|
10
|
+
Card.with(
|
11
|
+
subject: subject,
|
12
|
+
parent_card: parent_card,
|
13
|
+
time_in: nil,
|
14
|
+
time_out: nil
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def in(card)
|
19
|
+
card.with(time_in: time_now.call)
|
20
|
+
end
|
21
|
+
|
22
|
+
def out(card)
|
23
|
+
card.with(time_out: time_now.call)
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent_card_of(card)
|
27
|
+
card.parent_card
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/tick_tock.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "tick_tock/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "tick_tock"
|
7
|
+
spec.version = TickTock::VERSION
|
8
|
+
spec.authors = ["Marc Siegel"]
|
9
|
+
spec.email = ["marc@usainnov.com"]
|
10
|
+
|
11
|
+
spec.summary = "TickTock wraps Ruby code in timing and logging easily!"
|
12
|
+
spec.description = "TickTock makes it easy to wrap your Ruby code to "\
|
13
|
+
"measure nested timings and to log them -- even when "\
|
14
|
+
"the code is asynchronous or lazy, so straight-forward "\
|
15
|
+
"blocks do not work. Versions follow SemVer."
|
16
|
+
spec.homepage = "https://github.com/ms-ati/tick_tock"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.required_ruby_version = ">= 2.3.0"
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "values", ">= 1.8.0"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
30
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
|
33
|
+
spec.add_development_dependency "byebug"
|
34
|
+
|
35
|
+
spec.add_development_dependency "benchmark-ips"
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tick_tock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marc Siegel
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: values
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.8.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.8.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.16'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '12.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '12.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: benchmark-ips
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: TickTock makes it easy to wrap your Ruby code to measure nested timings
|
98
|
+
and to log them -- even when the code is asynchronous or lazy, so straight-forward
|
99
|
+
blocks do not work. Versions follow SemVer.
|
100
|
+
email:
|
101
|
+
- marc@usainnov.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- ".gitignore"
|
107
|
+
- ".rspec"
|
108
|
+
- ".travis.yml"
|
109
|
+
- Gemfile
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- bin/console
|
113
|
+
- bin/setup
|
114
|
+
- examples/lazy_example.rb
|
115
|
+
- examples/threaded_example.rb
|
116
|
+
- lib/tick_tock.rb
|
117
|
+
- lib/tick_tock/card.rb
|
118
|
+
- lib/tick_tock/card_logger.rb
|
119
|
+
- lib/tick_tock/clock.rb
|
120
|
+
- lib/tick_tock/locals.rb
|
121
|
+
- lib/tick_tock/punch.rb
|
122
|
+
- lib/tick_tock/version.rb
|
123
|
+
- tick_tock.gemspec
|
124
|
+
homepage: https://github.com/ms-ati/tick_tock
|
125
|
+
licenses: []
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 2.3.0
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 2.7.4
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: TickTock wraps Ruby code in timing and logging easily!
|
147
|
+
test_files: []
|