tick_tock 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://img.shields.io/travis/ms-ati/tick_tock/master.svg)](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: []
|