Class: Dash::Timings
- Inherits:
-
Object
- Object
- Dash::Timings
- Defined in:
- lib/dash/timings.rb
Overview
Wall-clock accounting for a deploy, printed under "Finished all in". The total on its own says nothing about where the time went — a serialised boot, a slow pull, or a proxy waiting on a health check all look the same from the outside.
Entries land in start order and carry a depth, so a parent phase (Boot) prints above the host entries it wraps even though the hosts finish first. Boot runs one thread per host, so every mutation is behind the mutex.
Each entry also accounts for the commands issued while it was the current phase, which
is what separates "the boot took 55s" from "the boot spent 41s of that in forty serial
SSH round trips". Attribution is by thread-local: phase marks its entry as current for
the duration of the block, and lib/dash/sshkit_with_ext.rb stamps every command and
every SSH connect onto whatever entry is current on that thread. Nothing extra is
executed — dash only measures the round trips it was already making.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- CURRENT_KEY =
The entry the current thread is inside. SSHKit's per-host threads inherit it from the thread that spawned them (see CompleteAll#execute and SSHKitDslRoles#on_roles), so a command issued on a boot thread lands on that host's row.
:dash_timing_entry
Class Method Summary collapse
- .current_entry ⇒ Object
- .current_entry=(entry) ⇒ Object
-
.from_h(phases) ⇒ Object
Rebuilds a table from what #to_h exported, so
dash reportcan print a saved deploy the way the deploy printed it.
Instance Method Summary collapse
- #any? ⇒ Boolean
- #attribute_command(seconds, local:) ⇒ Object
- #attribute_connect(seconds) ⇒ Object
- #current ⇒ Object
-
#entry_at(index) ⇒ Object
Where an entry's row sits in #lines, so Dash::Report can splice its build rows in under the phase they belong to.
- #index_of(entry) ⇒ Object
-
#initialize(entries: []) ⇒ Timings
constructor
A new instance of Timings.
- #lines ⇒ Object
-
#phase(name, depth: 0) ⇒ Object
Times the block.
-
#record(name, seconds, depth: 0, detail: nil) ⇒ Object
For time that was measured elsewhere — the gem load and config parse that happened before there was a Timings to record into.
-
#seconds_for(name) ⇒ Object
A top-level phase's wall time by name, for consumers that know a phase by what it is called rather than by where it sits — the post-deploy hook's build and boot runtimes.
-
#to_h ⇒ Object
The command counts here are subtree totals, matching what the table prints — phases nest, so a consumer must not sum them across depths.
Constructor Details
#initialize(entries: []) ⇒ Timings
Returns a new instance of Timings.
52 53 54 55 |
# File 'lib/dash/timings.rb', line 52 def initialize(entries: []) @entries = entries @mutex = Mutex.new end |
Class Method Details
.current_entry ⇒ Object
27 28 29 |
# File 'lib/dash/timings.rb', line 27 def current_entry Thread.current[CURRENT_KEY] end |
.current_entry=(entry) ⇒ Object
31 32 33 |
# File 'lib/dash/timings.rb', line 31 def current_entry=(entry) Thread.current[CURRENT_KEY] = entry end |
.from_h(phases) ⇒ Object
Rebuilds a table from what #to_h exported, so dash report can print a saved
deploy the way the deploy printed it. The entries come back parentless: their
counters are already the subtree totals #to_h computed, and re-nesting them would
roll those totals up a second time.
39 40 41 |
# File 'lib/dash/timings.rb', line 39 def from_h(phases) new entries: Array(phases).map { |phase| entry_from(phase.transform_keys(&:to_sym)) } end |
Instance Method Details
#any? ⇒ Boolean
108 109 110 |
# File 'lib/dash/timings.rb', line 108 def any? @mutex.synchronize { @entries.any? } end |
#attribute_command(seconds, local:) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/dash/timings.rb', line 90 def attribute_command(seconds, local:) entry = current or return @mutex.synchronize do entry.commands += 1 entry.command_seconds += seconds # A phase that issued even one remote command is reported as ssh: the local # label is only honest when nothing left the machine. entry.local &&= local end end |
#attribute_connect(seconds) ⇒ Object
102 103 104 105 106 |
# File 'lib/dash/timings.rb', line 102 def attribute_connect(seconds) entry = current or return @mutex.synchronize { entry.connect_seconds += seconds } end |
#current ⇒ Object
86 87 88 |
# File 'lib/dash/timings.rb', line 86 def current self.class.current_entry end |
#entry_at(index) ⇒ Object
Where an entry's row sits in #lines, so Dash::Report can splice its build rows in under the phase they belong to. Identity, not equality: two phases of the same name and duration are equal as Structs but are not the same row. The row at a position, so a consumer that saved an index (Dash::Report, reattaching its build rows to the phase they hung under) can find the entry again.
117 118 119 |
# File 'lib/dash/timings.rb', line 117 def entry_at(index) @mutex.synchronize { @entries[index] } if index end |
#index_of(entry) ⇒ Object
121 122 123 |
# File 'lib/dash/timings.rb', line 121 def index_of(entry) @mutex.synchronize { @entries.index { |candidate| candidate.equal?(entry) } } end |
#lines ⇒ Object
125 126 127 128 129 130 131 |
# File 'lib/dash/timings.rb', line 125 def lines with_totals do |entry, totals| line = format(" %s%-36s %6.1fs", " " * entry.depth, entry.name, entry.seconds.to_f) line += format(" %3d %-5s %5.1fs", totals[:commands], totals[:local] ? "local" : "ssh", totals[:command_seconds]) if totals[:commands] > 0 entry.detail ? "#{line} (#{entry.detail})" : line end end |
#phase(name, depth: 0) ⇒ Object
Times the block. The entry is yielded so the block can annotate it — a boot can say how long of its total was spent waiting for the container to become healthy.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/dash/timings.rb', line 59 def phase(name, depth: 0) entry = new_entry(name, depth: depth) @mutex.synchronize { @entries << entry } started = clock previous = self.class.current_entry self.class.current_entry = entry begin yield entry ensure self.class.current_entry = previous end ensure entry.seconds = clock - started end |
#record(name, seconds, depth: 0, detail: nil) ⇒ Object
For time that was measured elsewhere — the gem load and config parse that happened before there was a Timings to record into.
78 79 80 81 82 83 84 |
# File 'lib/dash/timings.rb', line 78 def record(name, seconds, depth: 0, detail: nil) new_entry(name, depth: depth).tap do |entry| entry.seconds = seconds entry.detail = detail @mutex.synchronize { @entries << entry } end end |
#seconds_for(name) ⇒ Object
A top-level phase's wall time by name, for consumers that know a phase by what it is called rather than by where it sits — the post-deploy hook's build and boot runtimes. Depth-0 only: a per-host row inside Boot is named after the host, but a role could be named "Boot" and must not be mistaken for the phase.
137 138 139 |
# File 'lib/dash/timings.rb', line 137 def seconds_for(name) @mutex.synchronize { @entries.find { |entry| entry.name == name && entry.depth.to_i.zero? }&.seconds } end |
#to_h ⇒ Object
The command counts here are subtree totals, matching what the table prints — phases nest, so a consumer must not sum them across depths.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/dash/timings.rb', line 143 def to_h with_totals do |entry, totals| { name: entry.name, depth: entry.depth, seconds: entry.seconds.to_f, detail: entry.detail, commands: totals[:commands], command_seconds: totals[:command_seconds], connect_seconds: totals[:connect_seconds], local: totals[:local] } end end |