duel-retained 0.1.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/CHANGELOG.md +36 -0
- data/LICENSE +21 -0
- data/README.md +155 -0
- data/lib/duel/retained/version.rb +7 -0
- data/lib/duel/retained.rb +150 -0
- data/sig/duel/retained.rbs +62 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 731c988c83f260d7d2b1f72f8bcaea04a3c2353d95eef6018acbf5107b6bc8f6
|
|
4
|
+
data.tar.gz: 0e482b0d4bbff58e2c459807da6378ce62e5874d38d6db7ce7e943ff2139448e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4de0db7edeaadcb3bc5848249b87e54f7746c82be8153eb84ebdb1022c0bac4af822b6d638756e930a376444d9aafdcba68e097f887e85f7f10a1243ba6ef943
|
|
7
|
+
data.tar.gz: 18f36afd00357c1d3250f5fd1dabc40efd8cba87aa6591c9c455d958ccd2b3b47ff01c64da61b369c1c8d6fb9c0f3631bffbc546da09893e17971c119202d574
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-24
|
|
11
|
+
|
|
12
|
+
The first release. duel-retained is a Duel plugin, open source under
|
|
13
|
+
the MIT license. It needs Duel 0.1 and Ruby 3.4 or later, and measures
|
|
14
|
+
on CRuby alone (on JRuby and TruffleRuby it leaves itself out).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `c.use :retained`: the `retained_objects` metric (CRuby), the
|
|
19
|
+
objects each benchmark's iterations leave alive after a full garbage
|
|
20
|
+
collection, with Duel's allowance of 16 a sample (for what's still on
|
|
21
|
+
the machine stack when it stops) and tolerance of one per two
|
|
22
|
+
iterations, both changed with `c.measure("retained_objects", ...)`.
|
|
23
|
+
It measures straight through `untimed` blocks. Where a sample retained
|
|
24
|
+
more than it's allowed (as `c.measure` leaves its allowance and
|
|
25
|
+
tolerance), the regression shows what, an iteration: the
|
|
26
|
+
objects' classes and where they were made, the three most (from
|
|
27
|
+
allocation tracing during the sample).
|
|
28
|
+
- `retained_objects` is in objects (`unit: %w[object objects]`), so
|
|
29
|
+
reports read "1 object → 2 objects an iteration" (the summary line,
|
|
30
|
+
"retained_objects 3→5531", leaves out the unit its name ends in).
|
|
31
|
+
- Mutation tested: mutant kills every change to the plugin's code, in
|
|
32
|
+
CI.
|
|
33
|
+
- RBS signatures in sig/, checked with Steep.
|
|
34
|
+
|
|
35
|
+
[Unreleased]: https://github.com/sferik/duel-ruby/compare/duel-retained-v0.1.0...HEAD
|
|
36
|
+
[0.1.0]: https://github.com/sferik/duel-ruby/releases/tag/duel-retained-v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erik Berlin
|
|
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
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# duel-retained
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sferik/duel-ruby)
|
|
4
|
+
[](https://github.com/sferik/duel-ruby/actions/workflows/plugin-retained.yml)
|
|
5
|
+
[](https://rubygems.org/gems/duel-retained)
|
|
6
|
+
|
|
7
|
+
A [Duel](https://github.com/sferik/duel-ruby) plugin: every benchmark is
|
|
8
|
+
judged on what its iterations leave alive too, the Ruby objects still
|
|
9
|
+
there after a full garbage collection.
|
|
10
|
+
|
|
11
|
+
| Metric | What it is | A regression |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `retained_objects` | the objects a sample's iterations left alive, beyond a few a sample (CRuby) | one more per two iterations |
|
|
14
|
+
|
|
15
|
+
So a leak, a cache that grows with every call, or memoization keyed by
|
|
16
|
+
input that never repeats, fails, though it allocates no more and takes
|
|
17
|
+
no longer than before. Allocations count what a benchmark makes, and CPU
|
|
18
|
+
time what it costs now, but neither sees what it keeps. Duel's `peak_memory`
|
|
19
|
+
does, coarsely: resident memory, in fresh workers, judged by the
|
|
20
|
+
megabyte. This counts the objects, exactly.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Gemfile
|
|
26
|
+
group :development do
|
|
27
|
+
gem "duel-retained"
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> [!NOTE]
|
|
32
|
+
> It measures on CRuby. On JRuby and TruffleRuby, whose garbage collectors
|
|
33
|
+
> can't be held off, or their live objects counted, it leaves itself out:
|
|
34
|
+
> benchmarks run, and are judged, as without it.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# .duel.rb
|
|
40
|
+
Duel.configure { |c| c.use :retained }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then a benchmark whose code keeps what it's given fails:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# lib/my_formatter.rb, the candidate's: memoized, and never forgets
|
|
47
|
+
FORMATTED = {}
|
|
48
|
+
def self.format(value) = FORMATTED[value] ||= value.round(2).to_s.rjust(10)
|
|
49
|
+
|
|
50
|
+
# duel/formatter_duel.rb
|
|
51
|
+
duel("format: a number") { MyFormatter.format(rand) }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
! format: a number 0.4s n=5528 ... retained_objects 3→5531 allocations 11056
|
|
56
|
+
REGRESSION format: a number: worse retained_objects in every measurement
|
|
57
|
+
retained_objects: 0.0005427 objects → 1.001 objects an iteration
|
|
58
|
+
retained_objects (candidate): an iteration: 1 String (lib/my_formatter.rb:3)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
What it retained beyond what's let through is shown by class, and where
|
|
62
|
+
it was made (the three most): see [What it retained](#what-it-retained).
|
|
63
|
+
|
|
64
|
+
Install it after any other plugin's metrics: those added after it are
|
|
65
|
+
read inside its measurement, and what they keep of their readings counts
|
|
66
|
+
toward its allowance.
|
|
67
|
+
|
|
68
|
+
A sample may leave 16 objects alive before any counts against it (its
|
|
69
|
+
allowance), and beyond that it's one more per two iterations (its
|
|
70
|
+
tolerance) that's a regression: see [below](#what-it-cant-help-counting).
|
|
71
|
+
Duel's `c.measure` changes either, or leaves the metric out:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
Duel.configure do |c|
|
|
75
|
+
c.use :retained
|
|
76
|
+
c.measure("retained_objects", allowance: 32, tolerance: 1) # or skip: true
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## How it measures
|
|
81
|
+
|
|
82
|
+
Duel holds the garbage collector off during a sample, so that when it
|
|
83
|
+
runs isn't up to chance. `GC.start` still collects (CRuby leaves the
|
|
84
|
+
collector disabled after): before each sample it counts in, the plugin
|
|
85
|
+
collects all the garbage there is, a full collection twice (what a
|
|
86
|
+
finalizer, or a dynamic symbol, keeps alive goes in the second), and
|
|
87
|
+
counts the live objects (`GC.stat(:heap_live_slots)`). After the sample,
|
|
88
|
+
it does it again. What's left is what the sample's iterations kept.
|
|
89
|
+
|
|
90
|
+
That's outside the built-in metrics, so none of it is in a benchmark's
|
|
91
|
+
allocations, instructions or CPU time. And only in the samples Duel
|
|
92
|
+
counts in (three of each version's a benchmark, and as many again for
|
|
93
|
+
each time it measures one again before failing), not the timed ones. A
|
|
94
|
+
full collection takes about half a millisecond with 30,000 live objects,
|
|
95
|
+
and some 15 with a million: four a sample, so a fraction of a second a
|
|
96
|
+
benchmark, for even a large application.
|
|
97
|
+
|
|
98
|
+
What's measured is the count itself, every object more after the sample
|
|
99
|
+
than before it (or fewer, for one that let go of more than it kept), and
|
|
100
|
+
Duel judges it: a candidate that leaves more alive than the baseline, by
|
|
101
|
+
more than the allowance a sample and the tolerance an iteration, fails.
|
|
102
|
+
|
|
103
|
+
### What it can't help counting
|
|
104
|
+
|
|
105
|
+
Not every object a sample leaves alive is the benchmark's. Duel's own
|
|
106
|
+
record of the metrics inside this one is. And so is what's still on the
|
|
107
|
+
machine stack when the sample stops: the last iteration's result (or the
|
|
108
|
+
block's, given n) is no longer referenced, but CRuby scans the stack
|
|
109
|
+
conservatively, so a stale reference to it there, or to anything it
|
|
110
|
+
holds, keeps it alive through the collection. Nothing Duel or the plugin
|
|
111
|
+
can do from Ruby clears it: running the iterations on a stack of their
|
|
112
|
+
own (a Fiber) would, but it would change what `Thread.current[]` means to
|
|
113
|
+
the benchmark. They're the same few from sample to sample, but not
|
|
114
|
+
always to the object, so a sample is allowed 16 before any is counted.
|
|
115
|
+
On top of that, it's one more per two iterations, not one more, that's a
|
|
116
|
+
regression. So for a benchmark that runs thousands of iterations a
|
|
117
|
+
sample, as most do, one object kept an iteration fails. For one that
|
|
118
|
+
runs a few (a slow one, or one with `only: :count`, which runs four),
|
|
119
|
+
it takes more an iteration.
|
|
120
|
+
|
|
121
|
+
What a benchmark makes the first time and keeps (a constant it
|
|
122
|
+
memoizes, a method cache) isn't a leak: Duel runs each benchmark many
|
|
123
|
+
times before it counts, and none of that is counted.
|
|
124
|
+
|
|
125
|
+
Benchmarks with `only: :timing` aren't counted at all, so they aren't
|
|
126
|
+
judged on this either.
|
|
127
|
+
|
|
128
|
+
### Untimed blocks
|
|
129
|
+
|
|
130
|
+
Duel pauses every metric around an `untimed` block, and this one would
|
|
131
|
+
collect the garbage, twice, on each: in a benchmark with an `untimed`
|
|
132
|
+
block in every iteration, that's thousands of collections a sample. So
|
|
133
|
+
it doesn't pause: it measures the whole sample, and what an `untimed`
|
|
134
|
+
block makes counts only if it's still alive when the sample is over.
|
|
135
|
+
What it usually makes, the input a benchmark works on, isn't.
|
|
136
|
+
|
|
137
|
+
## What it retained
|
|
138
|
+
|
|
139
|
+
Where a sample retained more than Duel lets through (its allowance, and
|
|
140
|
+
its tolerance for its iterations, as `c.measure` leaves them), the
|
|
141
|
+
plugin says what, under the regression: the objects' classes, and where
|
|
142
|
+
they were made, the three most, an iteration (`1 String
|
|
143
|
+
(lib/my_formatter.rb:3)`), of the sample whose count was judged, on
|
|
144
|
+
each version that did. Files are from the working directory, the
|
|
145
|
+
project's, so the baseline's show their whole path, in its checkout
|
|
146
|
+
next to the project. Objects Duel makes for itself aren't shown.
|
|
147
|
+
|
|
148
|
+
It knows from Ruby's allocation tracing (`objspace`'s
|
|
149
|
+
`ObjectSpace.trace_object_allocations`), on during each sample it counts
|
|
150
|
+
in: that makes each allocation some four times slower, in samples that
|
|
151
|
+
aren't timed. Telling what's left apart walks every live object (some
|
|
152
|
+
twenty times as long as a full collection), so it's done only for a
|
|
153
|
+
sample that retained more than Duel lets through. A benchmark that
|
|
154
|
+
traces allocations itself, or clears the trace, shares the one Ruby
|
|
155
|
+
has.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "duel"
|
|
4
|
+
require "objspace"
|
|
5
|
+
require_relative "retained/version"
|
|
6
|
+
|
|
7
|
+
module Duel
|
|
8
|
+
# What each benchmark leaves alive: the Ruby objects still there after
|
|
9
|
+
# a full garbage collection that weren't before it (CRuby).
|
|
10
|
+
#
|
|
11
|
+
# - retained_objects: the objects a sample's iterations left alive
|
|
12
|
+
# (counted; beyond ALLOWANCE a sample, one more per two iterations is
|
|
13
|
+
# a regression)
|
|
14
|
+
#
|
|
15
|
+
# # .duel.rb
|
|
16
|
+
# Duel.configure { |c| c.use :retained }
|
|
17
|
+
#
|
|
18
|
+
# So a leak, a cache that grows with every call, or memoization keyed
|
|
19
|
+
# by input that never repeats, fails, though it allocates no more and
|
|
20
|
+
# takes no longer than before.
|
|
21
|
+
#
|
|
22
|
+
# Duel holds the garbage collector off during a sample, but GC.start
|
|
23
|
+
# still collects: the metric collects, twice (what a finalizer or a
|
|
24
|
+
# dynamic symbol keeps alive goes in the second), and counts the live
|
|
25
|
+
# objects before the sample and again after it. It's outside the
|
|
26
|
+
# built-in metrics, so none of that is in allocations or CPU time, and
|
|
27
|
+
# only in the samples Duel counts in (a few a benchmark), not the timed
|
|
28
|
+
# ones. When a sample retained more than Duel lets through, the metric
|
|
29
|
+
# says what (Objects#details), by class and where it was made, under
|
|
30
|
+
# the regression: "an iteration: 1 String (lib/my_cache.rb:12)".
|
|
31
|
+
module Retained
|
|
32
|
+
Duel.plugin(:retained, self)
|
|
33
|
+
|
|
34
|
+
# The retained objects a sample is allowed (Duel's allowance) before
|
|
35
|
+
# any is counted against it: Duel's own record of the metrics inside
|
|
36
|
+
# this one, and what's still on the machine stack when the sample
|
|
37
|
+
# stops (the last iteration's result, or the block's, given n),
|
|
38
|
+
# which are the same few from sample to sample, but not always to the
|
|
39
|
+
# object.
|
|
40
|
+
ALLOWANCE = 16
|
|
41
|
+
|
|
42
|
+
# Retained objects per iteration that still aren't a regression.
|
|
43
|
+
TOLERANCE = 0.5
|
|
44
|
+
|
|
45
|
+
# The kinds of object (by class, and where they were made) a sample
|
|
46
|
+
# that retained more than its allowance shows, most first.
|
|
47
|
+
SHOWN = 3
|
|
48
|
+
|
|
49
|
+
# Configures Duel for the plugin (c.use :retained): the metric
|
|
50
|
+
# (CRuby only: elsewhere the garbage collector can't be held off or
|
|
51
|
+
# its live objects counted). Returns the metric.
|
|
52
|
+
def self.configure(config) = config.metric(Objects)
|
|
53
|
+
|
|
54
|
+
# Collects all the garbage there is: a full collection (GC.start's
|
|
55
|
+
# own: a full mark, and an immediate sweep), twice (the first leaves
|
|
56
|
+
# what has a finalizer to run, and a dynamic symbol's string, for the
|
|
57
|
+
# second). It collects though the garbage collector is disabled, and
|
|
58
|
+
# leaves it as it was.
|
|
59
|
+
def self.collect = 2.times { GC.start }
|
|
60
|
+
|
|
61
|
+
# The objects alive after collecting the garbage.
|
|
62
|
+
def self.live
|
|
63
|
+
collect
|
|
64
|
+
GC.stat(:heap_live_slots)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Where Duel's own files are: what it makes during a sample (its
|
|
68
|
+
# record of the metrics inside this one) is in the allowance, and
|
|
69
|
+
# isn't shown.
|
|
70
|
+
# (Duel's metric.rb, written in Ruby: it has a source location.)
|
|
71
|
+
metric_rb, = Metric.instance_method(:details).source_location #: [String, Integer]
|
|
72
|
+
DUEL = File.join(File.dirname(metric_rb), "")
|
|
73
|
+
|
|
74
|
+
# What each of the objects that a sample made (and Objects traced)
|
|
75
|
+
# and left alive is, and where it was made: {[class, "file:line"] =>
|
|
76
|
+
# how many}, most first, but Duel's own. A walk of every live object.
|
|
77
|
+
def self.traced
|
|
78
|
+
counts = Hash.new(0) #: Hash[[String, String], Integer]
|
|
79
|
+
class_of = Kernel.instance_method(:class)
|
|
80
|
+
here = File.join(Dir.pwd, "")
|
|
81
|
+
ObjectSpace.each_object do |object|
|
|
82
|
+
next unless ObjectSpace.allocation_generation(object)
|
|
83
|
+
|
|
84
|
+
file = ObjectSpace.allocation_sourcefile(object)
|
|
85
|
+
next if file.to_s.start_with?(DUEL)
|
|
86
|
+
|
|
87
|
+
klass = class_of.bind_call(object)
|
|
88
|
+
counts[[klass.name || klass.inspect, site(file, ObjectSpace.allocation_sourceline(object), here)]] += 1
|
|
89
|
+
end
|
|
90
|
+
counts.sort_by { |(klass, site), count| [-count, klass, site] }.to_h
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Where an object was made, from its +file+ and +line+ (nil where it
|
|
94
|
+
# wasn't made in Ruby code): the file from +here+ (a directory, with
|
|
95
|
+
# a trailing slash) where it's under it.
|
|
96
|
+
def self.site(file, line, here) = file ? "#{file.delete_prefix(here)}:#{line}" : "?"
|
|
97
|
+
|
|
98
|
+
# The metric: the objects alive after the sample less those before
|
|
99
|
+
# it (fewer, when the sample let go of more than it kept). Duel takes
|
|
100
|
+
# the allowance and the tolerance into account when it compares them.
|
|
101
|
+
class Objects < Metric
|
|
102
|
+
def self.measures = {"retained_objects" => {unit: %w[object objects], allowance: ALLOWANCE, tolerance: TOLERANCE}}
|
|
103
|
+
|
|
104
|
+
# mutant:disable -- mutant runs on CRuby, where this is always true
|
|
105
|
+
def self.available? = RUBY_ENGINE == "ruby"
|
|
106
|
+
|
|
107
|
+
# It traces where objects are made (ObjectSpace's allocation
|
|
108
|
+
# tracing) during the sample, for #details: what it retained, it
|
|
109
|
+
# can tell apart from the rest.
|
|
110
|
+
def start_sample(sample)
|
|
111
|
+
@n = sample.n
|
|
112
|
+
ObjectSpace.trace_object_allocations_clear
|
|
113
|
+
ObjectSpace.trace_object_allocations_start
|
|
114
|
+
@live = Retained.live
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def stop_sample
|
|
118
|
+
@retained = Retained.live - @live
|
|
119
|
+
ensure
|
|
120
|
+
ObjectSpace.trace_object_allocations_stop
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# It goes on through an untimed block: stopping and starting would
|
|
124
|
+
# collect the garbage, twice, on each, for every untimed block (an
|
|
125
|
+
# iteration's, maybe). So it measures the whole sample, and what an
|
|
126
|
+
# untimed block made (the input the benchmark works on, usually)
|
|
127
|
+
# counts only if it's still alive when the sample is over.
|
|
128
|
+
def pause_sample
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resume_sample(_sample)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# What the sample retained, an iteration, by class and where it was
|
|
135
|
+
# made (the SHOWN most), when it retained more than Duel lets through
|
|
136
|
+
# (its allowance a sample, and its tolerance an iteration, as
|
|
137
|
+
# configured): only then is it worth walking every live object for
|
|
138
|
+
# (Retained.traced). Nothing for one that retained less. Duel asks
|
|
139
|
+
# once, after the sample: the trace is cleared either way.
|
|
140
|
+
def details
|
|
141
|
+
return if within?(@retained, @n)
|
|
142
|
+
|
|
143
|
+
kinds = Retained.traced.first(SHOWN).map { |(klass, site), count| format("%.4g %s (%s)", count.fdiv(@n), klass, site) }
|
|
144
|
+
"an iteration: #{kinds.join(", ")}" unless kinds.empty? # (none it can see: internal objects, or a trace cleared)
|
|
145
|
+
ensure
|
|
146
|
+
ObjectSpace.trace_object_allocations_clear
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Duel
|
|
2
|
+
# What each benchmark leaves alive: the Ruby objects still there after
|
|
3
|
+
# a full garbage collection that weren't before it (retained_objects).
|
|
4
|
+
module Retained
|
|
5
|
+
VERSION: String
|
|
6
|
+
|
|
7
|
+
# The retained objects a sample is allowed (Duel's allowance) before
|
|
8
|
+
# any is counted against it.
|
|
9
|
+
ALLOWANCE: Integer
|
|
10
|
+
|
|
11
|
+
# Retained objects per iteration that still aren't a regression.
|
|
12
|
+
TOLERANCE: Float
|
|
13
|
+
|
|
14
|
+
# The kinds of object a sample that retained more than its allowance
|
|
15
|
+
# shows, most first.
|
|
16
|
+
SHOWN: Integer
|
|
17
|
+
|
|
18
|
+
# Configures Duel for the plugin (c.use :retained): the metric (CRuby
|
|
19
|
+
# only). Returns it.
|
|
20
|
+
def self.configure: (Config config) -> singleton(Metric)
|
|
21
|
+
|
|
22
|
+
# Collects all the garbage there is (a full collection, twice).
|
|
23
|
+
def self.collect: () -> void
|
|
24
|
+
|
|
25
|
+
# The objects alive after collecting the garbage.
|
|
26
|
+
def self.live: () -> Integer
|
|
27
|
+
|
|
28
|
+
# What each of the objects a sample made and left alive is, and where
|
|
29
|
+
# it was made: {[class, "file:line"] => how many}, most first.
|
|
30
|
+
def self.traced: () -> Hash[[String, String], Integer]
|
|
31
|
+
|
|
32
|
+
# Where Duel's own files are: what it makes isn't shown.
|
|
33
|
+
DUEL: String
|
|
34
|
+
|
|
35
|
+
# Where an object was made, from its +file+ and +line+ (nil where it
|
|
36
|
+
# wasn't made in Ruby code).
|
|
37
|
+
def self.site: (String? file, Integer? line, String here) -> String
|
|
38
|
+
|
|
39
|
+
# The metric: the objects alive after the sample less those before
|
|
40
|
+
# it.
|
|
41
|
+
class Objects < Metric
|
|
42
|
+
@live: Integer
|
|
43
|
+
@n: Integer
|
|
44
|
+
@retained: Integer
|
|
45
|
+
|
|
46
|
+
# It traces where objects are made during the sample, for #details.
|
|
47
|
+
def start_sample: (Sample sample) -> void
|
|
48
|
+
|
|
49
|
+
def stop_sample: () -> Integer
|
|
50
|
+
|
|
51
|
+
# It goes on through an untimed block: it measures the whole sample.
|
|
52
|
+
def pause_sample: () -> nil
|
|
53
|
+
|
|
54
|
+
def resume_sample: (Sample _sample) -> nil
|
|
55
|
+
|
|
56
|
+
# What the sample retained, an iteration, by class and where it was
|
|
57
|
+
# made, when it retained more than its allowance and tolerance let
|
|
58
|
+
# through.
|
|
59
|
+
def details: () -> String?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: duel-retained
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Erik Berlin
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: duel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
description: 'A Duel plugin: every benchmark is judged on the Ruby objects its iterations
|
|
27
|
+
leave alive after a full garbage collection, so a leak, or a cache that grows with
|
|
28
|
+
every call, fails.'
|
|
29
|
+
email:
|
|
30
|
+
- sferik@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/duel/retained.rb
|
|
39
|
+
- lib/duel/retained/version.rb
|
|
40
|
+
- sig/duel/retained.rbs
|
|
41
|
+
homepage: https://github.com/sferik/duel-ruby/tree/main/plugins/retained
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
source_code_uri: https://github.com/sferik/duel-ruby/tree/main/plugins/retained
|
|
46
|
+
changelog_uri: https://github.com/sferik/duel-ruby/blob/main/plugins/retained/CHANGELOG.md
|
|
47
|
+
bug_tracker_uri: https://github.com/sferik/duel-ruby/issues
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '3.4'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 4.0.23
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: 'What each Duel benchmark leaves alive: objects that outlive the garbage
|
|
66
|
+
collector'
|
|
67
|
+
test_files: []
|