timify 0.0.6 → 1.0.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 +4 -4
- data/.yardopts +6 -2
- data/CHANGELOG.md +41 -0
- data/LICENSE +1 -1
- data/README.md +233 -118
- data/lib/timify/recording.rb +375 -0
- data/lib/timify/registry.rb +31 -0
- data/lib/timify/report.rb +192 -0
- data/lib/timify/span.rb +71 -0
- data/lib/timify/trace.rb +27 -0
- data/lib/timify/version.rb +5 -0
- data/lib/timify.rb +136 -182
- metadata +21 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1903dbb0ac271c259becf4d7c99ccfaed2be1604f2279762e1544213d93fa1ff
|
|
4
|
+
data.tar.gz: 8cb993a3168f88f4ae6c9d2b5fb1d1a4ac7548d4e425cc44b91646321a637e58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a1516b808e8933021f8028f0edb2bd50419d6765959ae8c09b62254cac941fd1b68d5b0850e4c3809e80115bb7be0b3fd7f61554180b105eda69eddaab40c0e
|
|
7
|
+
data.tar.gz: 769d2406bd385599226d69007bc47c67e17c7a942fd65590582b847fdd7cf3cb54115b9699b1019a0ff762fedc2112ed8cfa847bc12b190b8f6a637f6b0377e2
|
data/.yardopts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
--readme README.md
|
|
2
|
-
--title 'timify -
|
|
2
|
+
--title 'timify - Calculate elapsed time between points in your code and report statistics.'
|
|
3
3
|
--charset utf-8
|
|
4
4
|
--markup markdown
|
|
5
|
-
|
|
5
|
+
lib/**/*.rb
|
|
6
|
+
-
|
|
7
|
+
README.md
|
|
8
|
+
CHANGELOG.md
|
|
9
|
+
LICENSE
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Timify#measure` and `Timify.measure` time a block. Nested measures and `add` calls inside a block form a span tree: `inclusive` is the whole block and `secs` is the time not already taken by its children.
|
|
8
|
+
- `Timify.trace` yields a timer and returns a `Trace` with both the block's value and the timer.
|
|
9
|
+
- `Timify.enabled` / `Timify.enabled?` is a process-wide off switch. `TIMIFY_DISABLE` (`1`, `true`, or `on`) sets the default when the class loads.
|
|
10
|
+
- `totals` includes `tree`, `grouped_tree`, `wall_time`, and `inclusive`, `p50`, `p95`, and `p99` on each locations, labels, and ranges entry. The printed summary shows the tree, p95, and inclusive time when it differs from self time. Flat sections are ordered slowest first. `totals(group: true)` also prints a grouped span tree.
|
|
11
|
+
- `on_slow` runs callbacks when a mark or a block crosses a threshold.
|
|
12
|
+
- `on_share` runs callbacks when a child span takes at least a given share of its parent's elapsed time so far.
|
|
13
|
+
- `Timify[:name]` reuses a quiet timer from anywhere in the process. `Timify.clear!` drops that registry.
|
|
14
|
+
- Each locations, labels, and ranges entry includes `min`, `max`, and `avg`.
|
|
15
|
+
- `Timify#reset` clears recorded segments and starts the clock again.
|
|
16
|
+
- `output:` sends printed lines to any object that responds to `puts`, or to a Logger via `info`.
|
|
17
|
+
- RSpec suite, YARD documentation, and a GitHub Actions build on Ruby 3.2, 3.3, 3.4, and 4.0.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `add :database` no longer raises `NoMethodError`. Symbol and string labels that read the same are grouped together.
|
|
22
|
+
- `show: false` suppresses the initialization line as well as later output.
|
|
23
|
+
- `status` defaults to `:on`.
|
|
24
|
+
- Time spent while `status` is `:off` is no longer charged to the next `add`. `measure` still runs its block while paused, and records nothing.
|
|
25
|
+
- `totals` keeps returning collected data while the timer is paused.
|
|
26
|
+
- Call sites come from `caller_locations` instead of a regular expression over `caller`. Paths under the working directory are relative, and the method name is included.
|
|
27
|
+
- Elapsed time uses a monotonic clock, so a wall-clock step cannot produce a negative duration.
|
|
28
|
+
- Label, location, and range counts are stored separately.
|
|
29
|
+
- Each thread keeps its own cursor and span stack. Shared totals are updated under a mutex.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Requires Ruby 3.2 or newer.
|
|
34
|
+
- The version constant is `Timify::VERSION`.
|
|
35
|
+
- There are still no runtime dependencies. `totals(json: true)` continues to use the standard-library `json`.
|
|
36
|
+
- `total_time` is the sum of self time. Parallel threads can make it larger than `wall_time`.
|
|
37
|
+
- Library code is split under `lib/timify/` (`span`, `trace`, `registry`, `recording`, `report`) with a short public shell in `lib/timify.rb`.
|
|
38
|
+
|
|
39
|
+
## 0.0.6 - 2021-05-27
|
|
40
|
+
|
|
41
|
+
- Last release published before 1.0.0. Earlier changes are in the GitHub history.
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# Timify
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/timify)
|
|
4
|
+
[](https://github.com/MarioRuiz/timify/actions/workflows/ci.yml)
|
|
5
|
+
[](https://coveralls.io/github/MarioRuiz/timify?branch=master)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
Timify measures elapsed time from one point in your Ruby code to another and reports it by call site, label, span tree, and the jump between call sites. Use it to see which part of a piece of code is taking the time.
|
|
12
|
+
|
|
13
|
+
Requires Ruby 3.2 or newer. The gem has no runtime dependencies.
|
|
6
14
|
|
|
7
15
|
## Installation
|
|
8
16
|
|
|
9
17
|
Add this line to your application's Gemfile:
|
|
10
18
|
|
|
11
19
|
```ruby
|
|
12
|
-
gem
|
|
20
|
+
gem "timify"
|
|
13
21
|
```
|
|
14
22
|
|
|
15
23
|
And then execute:
|
|
@@ -22,166 +30,273 @@ Or install it yourself as:
|
|
|
22
30
|
|
|
23
31
|
## Usage
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
The walkthrough below times a small "create user" path: open a timer, mark steps, nest spans, share the timer across call sites, then read the report.
|
|
34
|
+
|
|
35
|
+
### Create a timer
|
|
36
|
+
|
|
28
37
|
```ruby
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
require "logger"
|
|
39
|
+
|
|
40
|
+
timer = Timify.new(
|
|
41
|
+
:create_user,
|
|
42
|
+
show: true,
|
|
43
|
+
min_time_to_show: 0.3,
|
|
44
|
+
output: Logger.new($stdout)
|
|
45
|
+
)
|
|
32
46
|
```
|
|
33
|
-
min_time_to_show: minimum time to show the elapsed time when calling 'add' method
|
|
34
47
|
|
|
35
|
-
show
|
|
48
|
+
* `show` prints the initialization line, each recorded segment, and the summary from `totals`. It defaults to `true`. Set it to `false` and the timer stays silent while it keeps recording.
|
|
49
|
+
* `min_time_to_show` is the shortest segment, in seconds, that gets printed. Shorter segments are still recorded. It defaults to `0`.
|
|
50
|
+
* `output` is where those lines go. An IO receives `puts`. A Logger, or anything else that responds to `info` and not `puts`, receives `info`. It defaults to `$stdout`.
|
|
51
|
+
* `status` is `:on` by default. Set it to `:off` to pause, and back to `:on` to resume.
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
You can also set the options after construction:
|
|
38
54
|
|
|
39
55
|
```ruby
|
|
40
|
-
|
|
56
|
+
timer = Timify.new(:create_user)
|
|
57
|
+
timer.show = false
|
|
58
|
+
timer.min_time_to_show = 0.5
|
|
41
59
|
```
|
|
42
60
|
|
|
43
|
-
|
|
61
|
+
### Mark steps with `add`
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
`add` records the time since the previous mark (creation, reset, end of the last `add`/`measure`, or resume) and returns that segment as a Float number of seconds. Pass an optional label to group those segments. A symbol and a string that read the same, such as `:database` and `"database"`, are one group. `nil` and a blank string are ignored.
|
|
46
64
|
|
|
47
65
|
```ruby
|
|
48
|
-
|
|
66
|
+
timer = Timify.new(:create_user, show: false)
|
|
67
|
+
timer.add
|
|
68
|
+
sleep 0.01
|
|
69
|
+
database_seconds = timer.add(:database)
|
|
70
|
+
timer.add(:mail) if database_seconds > 0.2
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The first `add` has no range. Each later `add` records the jump from the previous call site to the current one.
|
|
74
|
+
|
|
75
|
+
A printed segment looks like this. Paths under the working directory are relative, and the method name is included when present. The percentage is that call site's share of the self time recorded so far. The two numbers are the timer's total self seconds, then this segment's self seconds. `(New Max)` appears when this segment is the longest one so far. A 1.0 second `:database` segment followed by a 0.5 second `:mail` segment prints:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
<create_user><database>(New Max): app/create_user.rb:10 in create (100%): 1.0; 1.0
|
|
79
|
+
<create_user><mail>: app/create_user.rb:20 in create (33%): 1.5; 0.5
|
|
49
80
|
```
|
|
50
81
|
|
|
51
|
-
###
|
|
82
|
+
### Nested `measure`
|
|
83
|
+
|
|
84
|
+
`measure` times the given block and returns the block's value. Nested `measure` and `add` calls form a span tree. A parent span's inclusive time is the whole block; its self time (`secs`) is what the children did not already take. When time has passed since the previous mark, that gap is stored first as an unlabeled segment (printed as `(gap)`), and the block is stored next. A later `add` does not count the block again.
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
`add` and `measure` share one timeline. A mark made inside the block keeps its own slice, and `measure` records only the remainder, so the same seconds are not stored twice. The block itself is not a range. If the block raises, the time until the exception is still recorded and the exception propagates. While the timer is paused, the block runs and nothing new is recorded.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
timer = Timify.new(:create_user, show: false)
|
|
90
|
+
timer.measure(:request) do
|
|
91
|
+
timer.measure(:database) { :saved }
|
|
92
|
+
timer.add(:mail)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
With clocks that open `request` at 0, open `database` at 0.25 (after a 0.25 gap), close `database` at 0.75, and close `request` at 1.0, the labels report `request` as 0.25 self / 1.0 inclusive and `database` as 0.5 self / 0.5 inclusive. The tree children under `request` are the gap and `database`.
|
|
97
|
+
|
|
98
|
+
`Timify.measure` builds a timer, yields it, and returns it when the block completes. It does not return the block's value.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
timer = Timify.measure(:create_user, show: false) do |job|
|
|
102
|
+
job.measure(:database) { :saved }
|
|
103
|
+
job.add(:mail)
|
|
104
|
+
end
|
|
54
105
|
|
|
55
|
-
|
|
106
|
+
timer.totals
|
|
107
|
+
```
|
|
56
108
|
|
|
57
|
-
|
|
109
|
+
### `Timify.trace`
|
|
58
110
|
|
|
59
|
-
|
|
111
|
+
`Timify.trace` is the same setup as `Timify.measure`, but returns a `Timify::Trace` that keeps both the block's value and the timer. Options are the same keyword args as `Timify.new`. If the block raises, the exception propagates and no `Trace` is returned.
|
|
60
112
|
|
|
61
113
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
#some processes
|
|
70
|
-
do_log(t.totals[:message]) if t.add > 0.5
|
|
114
|
+
trace = Timify.trace(:create_user, show: false) do |timer|
|
|
115
|
+
timer.measure(:database) { :saved }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
trace.value # => :saved
|
|
119
|
+
trace.timer # => the Timify instance
|
|
120
|
+
trace.totals # => same as trace.timer.totals
|
|
71
121
|
```
|
|
72
122
|
|
|
73
|
-
|
|
123
|
+
`Timify.measure`, `Timify.trace`, and instance `measure` raise `ArgumentError` when no block is given.
|
|
74
124
|
|
|
75
|
-
|
|
125
|
+
### Reuse a named timer
|
|
76
126
|
|
|
77
|
-
|
|
127
|
+
`Timify[:name]` returns a quiet process-wide timer (`show: false` on first create) so two distant call sites can contribute to the same report. `Timify.clear!` drops that registry.
|
|
78
128
|
|
|
79
|
-
|
|
129
|
+
```ruby
|
|
130
|
+
def load_row
|
|
131
|
+
Timify[:create_user].measure(:database) { :row }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def send_mail
|
|
135
|
+
Timify[:create_user].add(:mail)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
load_row
|
|
139
|
+
send_mail
|
|
140
|
+
Timify[:create_user].totals
|
|
141
|
+
Timify.clear!
|
|
142
|
+
```
|
|
80
143
|
|
|
81
|
-
|
|
144
|
+
### Pause recording
|
|
82
145
|
|
|
83
|
-
|
|
146
|
+
`status` is `:on` by default. Set it to `:off` to pause, and back to `:on` to resume. Paused time is not charged to the next segment. The block still runs. `totals` still returns what was recorded before the pause.
|
|
84
147
|
|
|
85
|
-
|
|
148
|
+
```ruby
|
|
149
|
+
timer = Timify.new(:create_user, show: false)
|
|
150
|
+
timer.add(:setup)
|
|
86
151
|
|
|
87
|
-
|
|
152
|
+
timer.status = :off
|
|
153
|
+
result = timer.measure(:skipped) { :still_runs } # => :still_runs, nothing recorded
|
|
154
|
+
timer.add(:also_skipped) # => 0.0
|
|
88
155
|
|
|
89
|
-
|
|
156
|
+
timer.status = :on
|
|
157
|
+
timer.add(:mail) # charges only time since resume, not the pause
|
|
158
|
+
```
|
|
90
159
|
|
|
91
|
-
|
|
160
|
+
### Turn Timify off for the whole process
|
|
92
161
|
|
|
93
|
-
|
|
162
|
+
`Timify.enabled` is a process-wide switch. When it is `false`, every timer is a no-op: `add` returns `0.0` and records nothing, `measure` still runs its block and returns the value but records nothing and does not print, and `new` skips the init line. `totals` still returns whatever was recorded before the switch. `Timify.enabled?` returns the current flag.
|
|
94
163
|
|
|
95
|
-
|
|
164
|
+
The default comes from the environment variable `TIMIFY_DISABLE` when the class loads. Values `1`, `true`, and `on` (case insensitive) disable recording. Setting `Timify.enabled = true` afterwards overrides the env var for the rest of the process. The default is `true` when the variable is unset.
|
|
96
165
|
|
|
97
|
-
|
|
166
|
+
```ruby
|
|
167
|
+
# shell: TIMIFY_DISABLE=1 bundle exec ruby app.rb
|
|
168
|
+
# or in Ruby, after the gem has loaded:
|
|
169
|
+
Timify.enabled = false
|
|
170
|
+
timer = Timify.new(:create_user) # no init line
|
|
171
|
+
timer.measure(:database) { :saved } # => :saved, nothing recorded
|
|
172
|
+
timer.totals # empty report
|
|
173
|
+
|
|
174
|
+
Timify.enabled = true
|
|
175
|
+
timer.add(:mail) # recording resumes
|
|
176
|
+
```
|
|
98
177
|
|
|
99
|
-
|
|
178
|
+
### React to slow or dominant spans
|
|
100
179
|
|
|
101
|
-
|
|
180
|
+
`on_slow` registers a callback that runs when a segment is at least the given number of seconds long. For `add`, the segment length is compared. For `measure`, the block's inclusive time is compared. Several callbacks can be registered. They run after the segment is recorded, outside the timer mutex.
|
|
102
181
|
|
|
103
|
-
|
|
182
|
+
```ruby
|
|
183
|
+
timer = Timify.new(:create_user, show: false)
|
|
184
|
+
timer.on_slow(0.5) do |event|
|
|
185
|
+
warn "#{event.name} #{event.label} took #{event.inclusive}s at #{event.location}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
timer.measure(:database) { :row }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`on_share` registers a callback that runs when a child span's inclusive time is at least `ratio` of the parent's elapsed time so far (`ratio` is a Float from `0` to `1` inclusive). It fires for labeled `measure` and `add` children, not for root spans or unlabeled gap spans. Parent elapsed is the time from the parent's start to this moment, so a child that already dominates the parent so far still fires even though the parent has not closed. Several callbacks can be registered and are kept across `reset`, same as `on_slow`. They run outside the timer mutex.
|
|
104
192
|
|
|
193
|
+
The event is a `Timify::Event` with the usual fields plus `share` (Float) and `parent_label`. For `on_slow` events those two fields are `nil`.
|
|
105
194
|
|
|
106
|
-
Example of output:
|
|
107
195
|
```ruby
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"/add_customer.rb:581"=>{:secs=>0.446045, :percent=>10, :count=>1},
|
|
118
|
-
"/add_customer.rb:583"=>{:secs=>3.789378, :percent=>85, :count=>1},
|
|
119
|
-
"/add_customer.rb:585"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
120
|
-
"/add_customer.rb:587"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
121
|
-
"/add_customer.rb:595"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
122
|
-
"/add_customer.rb:603"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
123
|
-
"/add_customer.rb:612"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
124
|
-
"/add_customer.rb:617"=>{:secs=>0.0, :percent=>0, :count=>1}
|
|
125
|
-
},
|
|
126
|
-
:labels=>{
|
|
127
|
-
:database_access=>{:secs=>4,447444, :percent=>99, :count=>3},
|
|
128
|
-
:checkouts=>{:secs=>0.0, :percent=>0, :count=>2},
|
|
129
|
-
},
|
|
130
|
-
:ranges=>{
|
|
131
|
-
"/add_customer.rb:509 - /add_customer.rb:529"=>{:secs=>0.008001, :percent=>0, :count=>1},
|
|
132
|
-
"/add_customer.rb:529 - /add_customer.rb:532"=>{:secs=>0.212021, :percent=>5, :count=>1},
|
|
133
|
-
"/add_customer.rb:532 - /add_customer.rb:569"=>{:secs=>0.006001, :percent=>0, :count=>1},
|
|
134
|
-
"/add_customer.rb:569 - /add_customer.rb:581"=>{:secs=>0.446045, :percent=>10, :count=>1},
|
|
135
|
-
"/add_customer.rb:581 - /add_customer.rb:583"=>{:secs=>3.789378, :percent=>85, :count=>1},
|
|
136
|
-
"/add_customer.rb:583 - /add_customer.rb:585"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
137
|
-
"/add_customer.rb:585 - /add_customer.rb:587"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
138
|
-
"/add_customer.rb:587 - /add_customer.rb:595"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
139
|
-
"/add_customer.rb:595 - /add_customer.rb:603"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
140
|
-
"/add_customer.rb:603 - /add_customer.rb:612"=>{:secs=>0.0, :percent=>0, :count=>1},
|
|
141
|
-
"/add_customer.rb:612 - /add_customer.rb:617"=>{:secs=>0.0, :percent=>0, :count=>1}
|
|
142
|
-
},
|
|
143
|
-
:message=>"
|
|
144
|
-
|
|
145
|
-
Total time <add_customer_wrong>:4.46
|
|
146
|
-
Total time by location:
|
|
147
|
-
/add_customer.rb:509: 0.0 (0%) #1
|
|
148
|
-
/add_customer.rb:529: 0.01 (0%) #1
|
|
149
|
-
/add_customer.rb:532: 0.21 (5%) #1
|
|
150
|
-
/add_customer.rb:569: 0.01 (0%) #1
|
|
151
|
-
/add_customer.rb:581: 0.45 (10%) #1
|
|
152
|
-
/add_customer.rb:583: 3.79 (85%) #1
|
|
153
|
-
/add_customer.rb:585: 0.0 (0%) #1
|
|
154
|
-
/add_customer.rb:587: 0.0 (0%) #1
|
|
155
|
-
/add_customer.rb:595: 0.0 (0%) #1
|
|
156
|
-
/add_customer.rb:603: 0.0 (0%) #1
|
|
157
|
-
/add_customer.rb:612: 0.0 (0%) #1
|
|
158
|
-
/add_customer.rb:617: 0.0 (0%) #1
|
|
159
|
-
Total time by label:
|
|
160
|
-
database_access: 4.45 (99%) #3
|
|
161
|
-
checkouts: 0.0 (0%) #2
|
|
162
|
-
Total time by range:
|
|
163
|
-
/add_customer.rb:509 - /add_customer.rb:529: 0.01 (0%) #1
|
|
164
|
-
/add_customer.rb:529 - /add_customer.rb:532: 0.21 (5%) #1
|
|
165
|
-
/add_customer.rb:532 - /add_customer.rb:569: 0.01 (0%) #1
|
|
166
|
-
/add_customer.rb:569 - /add_customer.rb:581: 0.45 (10%) #1
|
|
167
|
-
/add_customer.rb:581 - /add_customer.rb:583: 3.79 (85%) #1
|
|
168
|
-
/add_customer.rb:583 - /add_customer.rb:585: 0.0 (0%) #1
|
|
169
|
-
/add_customer.rb:585 - /add_customer.rb:587: 0.0 (0%) #1
|
|
170
|
-
/add_customer.rb:587 - /add_customer.rb:595: 0.0 (0%) #1
|
|
171
|
-
/add_customer.rb:595 - /add_customer.rb:603: 0.0 (0%) #1
|
|
172
|
-
/add_customer.rb:603 - /add_customer.rb:612: 0.0 (0%) #1
|
|
173
|
-
/add_customer.rb:612 - /add_customer.rb:617: 0.0 (0%) #1
|
|
174
|
-
"
|
|
175
|
-
}
|
|
196
|
+
timer = Timify.new(:create_user, show: false)
|
|
197
|
+
timer.on_share(0.5) do |event|
|
|
198
|
+
warn "#{event.label} took #{(event.share * 100).round}% of #{event.parent_label}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Parent opens at 0, database closes at 0.8, parent closes at 1.0 → share 0.8, fires.
|
|
202
|
+
timer.measure(:request) do
|
|
203
|
+
timer.measure(:database) { :row }
|
|
204
|
+
end
|
|
176
205
|
```
|
|
177
206
|
|
|
207
|
+
### Read the report with `totals`
|
|
178
208
|
|
|
179
|
-
|
|
209
|
+
`totals` returns a Hash. `secs` is self time (what this span or bucket spent itself). `inclusive` is wall time covered by that span including children. Flat sections (`locations`, `labels`, `ranges`) are ordered slowest-first by self time.
|
|
180
210
|
|
|
181
|
-
|
|
211
|
+
`totals(group: true)` also prints a `Grouped spans:` section after the chronological Spans section; the default `group: false` does not. `grouped_tree` is always in the hash either way: sibling nodes with the same label are merged recursively (sums `secs` and `inclusive`, sets `count`, keeps the first location, groups children the same way). Nil labels (gaps) merge with other nil labels. Order of first appearance is preserved.
|
|
182
212
|
|
|
213
|
+
`totals(json: true)` returns the same report as a JSON string and still prints the summary when `show` is true. In JSON, hash keys and a symbol `name` are strings, and `started` and `finished` are time strings.
|
|
183
214
|
|
|
184
|
-
|
|
215
|
+
The hash contains:
|
|
216
|
+
|
|
217
|
+
* `name` — the name passed to `new`
|
|
218
|
+
* `total_time` — sum of self time recorded so far, excluding paused time. Parallel threads can make this larger than `wall_time`.
|
|
219
|
+
* `wall_time` — monotonic time from the start to the latest mark, minus pauses
|
|
220
|
+
* `started` — wall-clock `Time` when the timer was created or last reset
|
|
221
|
+
* `finished` — wall-clock `Time` of the latest recorded segment. It equals `started` until the first segment.
|
|
222
|
+
* `tree` — chronological top-level spans. Each node has `label`, `location`, `secs`, `inclusive`, and `children`.
|
|
223
|
+
* `grouped_tree` — merged sibling labels as described above
|
|
224
|
+
* `message` — the printable summary
|
|
225
|
+
* `locations` — one entry for each relative (or absolute) `path:line in method` where `add` or `measure` was called
|
|
226
|
+
* `labels` — one entry for each label
|
|
227
|
+
* `ranges` — one entry for each jump from one call site to the next. A `measure` block is not itself a range; the approach up to that call is, once a previous call site exists.
|
|
185
228
|
|
|
186
|
-
|
|
229
|
+
Each of `locations`, `labels`, and `ranges` contains `secs`, `inclusive`, `percent`, `count`, `min`, `max`, `avg`, `p50`, `p95`, `p99`, and optionally `samples_truncated` when more than 10_000 samples were seen for that bucket.
|
|
230
|
+
|
|
231
|
+
Elapsed time uses a monotonic clock. `started` and `finished` stay wall-clock times. Each thread keeps its own cursor and span stack; shared totals are updated under a mutex.
|
|
232
|
+
|
|
233
|
+
After two `add` calls that took 1.0 seconds and then 0.5 seconds:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
report = timer.totals
|
|
237
|
+
report[:total_time] # => 1.5
|
|
238
|
+
report[:labels].keys # => ["database", "mail"] (slowest first)
|
|
239
|
+
report[:labels]["database"] # => { secs: 1.0, inclusive: 1.0, percent: 67, count: 1, ... }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`message` is the text `totals` prints when `show` is true:
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
Total time <create_user>:1.5 wall 1.5
|
|
246
|
+
Spans:
|
|
247
|
+
database inclusive 1.0 self 1.0
|
|
248
|
+
mail inclusive 0.5 self 0.5
|
|
249
|
+
Total time by location:
|
|
250
|
+
app/create_user.rb:10 in create: 1.0 (67%) #1 min 1.0 max 1.0 avg 1.0 p95 1.0
|
|
251
|
+
app/create_user.rb:20 in create: 0.5 (33%) #1 min 0.5 max 0.5 avg 0.5 p95 0.5
|
|
252
|
+
|
|
253
|
+
Total time by label:
|
|
254
|
+
database: 1.0 (67%) #1 min 1.0 max 1.0 avg 1.0 p95 1.0
|
|
255
|
+
mail: 0.5 (33%) #1 min 0.5 max 0.5 avg 0.5 p95 0.5
|
|
256
|
+
|
|
257
|
+
Total time by range:
|
|
258
|
+
app/create_user.rb:10 in create - app/create_user.rb:20 in create: 0.5 (33%) #1 min 0.5 max 0.5 avg 0.5 p95 0.5
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When inclusive time differs from self time, bucket lines append ` inclusive N`.
|
|
262
|
+
|
|
263
|
+
Two sibling `measure(:database)` calls that took 0.3 seconds and 0.2 seconds keep two nodes in `tree` and one merged node in `grouped_tree`:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
report = timer.totals(group: true)
|
|
267
|
+
report[:tree].size # => 2
|
|
268
|
+
report[:grouped_tree].first # => { label: "database", ..., secs: 0.5, inclusive: 0.5, count: 2, children: [] }
|
|
269
|
+
# message includes "Grouped spans:" then "database inclusive 0.5 self 0.5 #2"
|
|
270
|
+
|
|
271
|
+
json = timer.totals(json: true) # => JSON string of the same report
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Start over with `reset`
|
|
275
|
+
|
|
276
|
+
`reset` drops every recorded segment and starts the clock again. The name, `show`, `min_time_to_show`, `output`, `on_slow`, and `on_share` callbacks stay as they are. A paused timer is left running.
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
timer = Timify.new(:create_user, show: false)
|
|
280
|
+
timer.add(:database)
|
|
281
|
+
timer.reset
|
|
282
|
+
timer.totals[:total_time] # => 0.0
|
|
283
|
+
timer.add(:mail) # new clock from reset
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Development
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
bundle install
|
|
290
|
+
bundle exec rake
|
|
291
|
+
bundle exec yard
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`rake` runs the RSpec suite. Development dependencies are Rake, RSpec, and YARD. CI runs that suite on Ruby 3.2, 3.3, 3.4, and 4.0.
|
|
295
|
+
|
|
296
|
+
## Contributing
|
|
297
|
+
|
|
298
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/MarioRuiz/timify.
|
|
299
|
+
|
|
300
|
+
## License
|
|
187
301
|
|
|
302
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|