timify 0.0.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 224c8980c101be7c0b632430f760cd4902cd0e7e80893c1971ee0755c45984d4
4
- data.tar.gz: e2f6e3e167cd555a6a9d11ecacf55a19895d26e7f5cd92aee0412ee41e8ff5cf
3
+ metadata.gz: 1903dbb0ac271c259becf4d7c99ccfaed2be1604f2279762e1544213d93fa1ff
4
+ data.tar.gz: 8cb993a3168f88f4ae6c9d2b5fb1d1a4ac7548d4e425cc44b91646321a637e58
5
5
  SHA512:
6
- metadata.gz: a96a8730523624686027f4678d4fe48420aa8a96a25203dd11f1445a09ca155957061f7801365f51c0a290ffe13b377f2763b3fe3988c1ae2d414571ce4cda6d
7
- data.tar.gz: decee0e39f1b680907094a5eafe11bd05bc3127cb22ca7e7c320e1818eed13362d8fa04a0aca00f39fbf8b9234135e8b629059509cefcbcb284e9a72e2774ccd
6
+ metadata.gz: 5a1516b808e8933021f8028f0edb2bd50419d6765959ae8c09b62254cac941fd1b68d5b0850e4c3809e80115bb7be0b3fd7f61554180b105eda69eddaab40c0e
7
+ data.tar.gz: 769d2406bd385599226d69007bc47c67e17c7a942fd65590582b847fdd7cf3cb54115b9699b1019a0ff762fedc2112ed8cfa847bc12b190b8f6a637f6b0377e2
data/.yardopts CHANGED
@@ -1,5 +1,9 @@
1
- --readme README.md
2
- --title 'timify - Easily calculates the time running (elapsed time) from one location to another inside your code and reports statistics.'
3
- --charset utf-8
4
- --markup markdown
5
- 'lib/**/*.rb' - '*.md' - 'LICENSE'
1
+ --readme README.md
2
+ --title 'timify - Calculate elapsed time between points in your code and report statistics.'
3
+ --charset utf-8
4
+ --markup markdown
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
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2017 Mario Ruiz
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2017-2026 Mario Ruiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,187 +1,302 @@
1
- # Timify
2
-
3
- [![Gem Version](https://badge.fury.io/rb/timify.svg)](https://rubygems.org/gems/timify)
4
-
5
- Easily calculates the time running (elapsed time) from one location to another inside your code and reports statistics. It helps you improve your code and find out which part of your code is consuming more time.
6
-
7
- ## Installation
8
-
9
- Add this line to your application's Gemfile:
10
-
11
- ```ruby
12
- gem 'timify'
13
- ```
14
-
15
- And then execute:
16
-
17
- $ bundle
18
-
19
- Or install it yourself as:
20
-
21
- $ gem install timify
22
-
23
- ## Usage
24
-
25
- ### initialize
26
- You need to supply a name for your Timify instance.
27
- You can have all the Timify instances you want at the same time.
28
- ```ruby
29
- t = Timify.new :create_user
30
- t.show = false
31
- t.min_time_to_show = 0.5
32
- ```
33
- min_time_to_show: minimum time to show the elapsed time when calling 'add' method
34
-
35
- show: print out results on screen
36
-
37
- you can supply those parameters also:
38
-
39
- ```ruby
40
- t = Timify.new :create_user, show: true, min_time_to_show: 0.3
41
- ```
42
-
43
- The scopes of the instances can be even global so you can measure the elapsed times between different classes, methods... on your code.
44
-
45
- You can disable the timify counter whenever you want using the status attritute that admits :on or :off, by default is set to :on
46
-
47
- ```ruby
48
- t.status = :off
49
- ```
50
-
51
- ### add
52
-
53
- Adds a new point to count the elapsed time. It will count from the last 'add' call or Timify creation in case of the first 'add'.
54
-
55
- You can supply a label that will summarize all the ones with the same label
56
-
57
- The output of this method will be the time elapsed in seconds (float).
58
-
59
- Examples:
60
-
61
- ```ruby
62
- t=Timify.new :example
63
- t.add; run_sqls; t.add :database
64
- t.add
65
- #some processes
66
- t.add
67
- #some processes
68
- send_email_alert(t.totals[:message]) if t.add > 0.2
69
- #some processes
70
- do_log(t.totals[:message]) if t.add > 0.5
71
- ```
72
-
73
- ### totals
74
-
75
- Returns all data for this instance
76
-
77
- In case json parameter supplied as true, the output will be in json format instead of a hash.
78
-
79
- The output hash contains:
80
-
81
- name: (String) name given for this instance'
82
-
83
- total_time: (float) total elapsed time from initialization to last 'add' call
84
-
85
- started: (Time)
86
-
87
- finished: (Time)
88
-
89
- message: (String) a printable friendly message giving all information
90
-
91
- locations, labels, ranges: (Hash) the resultant hash contains:
92
-
93
- secs: (float) number of seconds
94
-
95
- percent: (integer) percentage in reference to the total time
96
-
97
- count: (integer) number of times
98
-
99
- locations: (Hash) All summary data by location where was called
100
-
101
- labels: (Hash) All summary data by label given on 'add' method
102
-
103
- ranges: (Hash) All summary data by ranges where was called, from last 'add' call to current 'add' call
104
-
105
-
106
- Example of output:
107
- ```ruby
108
- { :name=>:add_customer_wrong,
109
- :total_time=>4.461446,
110
- :started=>2017-11-06 16:10:53 +0000,
111
- :finished=>2017-11-06 16:10:57 +0000,
112
- :locations=>{
113
- "/add_customer.rb:509"=>{:secs=>0.0, :percent=>0, :count=>1},
114
- "/add_customer.rb:529"=>{:secs=>0.008001, :percent=>0, :count=>1},
115
- "/add_customer.rb:532"=>{:secs=>0.212021, :percent=>5, :count=>1},
116
- "/add_customer.rb:569"=>{:secs=>0.006001, :percent=>0, :count=>1},
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
- }
176
- ```
177
-
178
-
179
- ## Contributing
180
-
181
- Bug reports and pull requests are welcome on GitHub at https://github.com/marioruiz/timify.
182
-
183
-
184
- ## License
185
-
186
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
187
-
1
+ # Timify
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/timify.svg)](https://rubygems.org/gems/timify)
4
+ [![CI](https://github.com/MarioRuiz/timify/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/MarioRuiz/timify/actions/workflows/ci.yml)
5
+ [![Coverage Status](https://coveralls.io/repos/github/MarioRuiz/timify/badge.svg?branch=master)](https://coveralls.io/github/MarioRuiz/timify?branch=master)
6
+ ![Gem](https://img.shields.io/gem/dt/timify)
7
+ ![GitHub commit activity](https://img.shields.io/github/commit-activity/y/MarioRuiz/timify)
8
+ ![GitHub last commit](https://img.shields.io/github/last-commit/MarioRuiz/timify)
9
+ ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/MarioRuiz/timify)
10
+
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.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem "timify"
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install timify
30
+
31
+ ## Usage
32
+
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
+
37
+ ```ruby
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
+ )
46
+ ```
47
+
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.
52
+
53
+ You can also set the options after construction:
54
+
55
+ ```ruby
56
+ timer = Timify.new(:create_user)
57
+ timer.show = false
58
+ timer.min_time_to_show = 0.5
59
+ ```
60
+
61
+ ### Mark steps with `add`
62
+
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.
64
+
65
+ ```ruby
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
80
+ ```
81
+
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.
85
+
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
105
+
106
+ timer.totals
107
+ ```
108
+
109
+ ### `Timify.trace`
110
+
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.
112
+
113
+ ```ruby
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
121
+ ```
122
+
123
+ `Timify.measure`, `Timify.trace`, and instance `measure` raise `ArgumentError` when no block is given.
124
+
125
+ ### Reuse a named timer
126
+
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.
128
+
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
+ ```
143
+
144
+ ### Pause recording
145
+
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.
147
+
148
+ ```ruby
149
+ timer = Timify.new(:create_user, show: false)
150
+ timer.add(:setup)
151
+
152
+ timer.status = :off
153
+ result = timer.measure(:skipped) { :still_runs } # => :still_runs, nothing recorded
154
+ timer.add(:also_skipped) # => 0.0
155
+
156
+ timer.status = :on
157
+ timer.add(:mail) # charges only time since resume, not the pause
158
+ ```
159
+
160
+ ### Turn Timify off for the whole process
161
+
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.
163
+
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.
165
+
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
+ ```
177
+
178
+ ### React to slow or dominant spans
179
+
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.
181
+
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.
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`.
194
+
195
+ ```ruby
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
205
+ ```
206
+
207
+ ### Read the report with `totals`
208
+
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.
210
+
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.
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.
214
+
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.
228
+
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
301
+
302
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).