rails-profiler 0.36.2 → 0.38.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.
@@ -9,6 +9,8 @@ module Profiler
9
9
  # dashboard relies on the default: http profiles, as summaries.
10
10
  if params[:parent_token].present?
11
11
  render_children_page
12
+ elsif params[:trace_id].present?
13
+ render_trace_page
12
14
  elsif all_types?
13
15
  render_profile_page(type: profile_type_param, summary: false)
14
16
  else
@@ -57,6 +59,14 @@ module Profiler
57
59
  params[:type].to_s.match?(/\A[a-z_]{1,32}\z/) ? params[:type].to_s : nil
58
60
  end
59
61
 
62
+ # The profiles of a trace, oldest last, read through the store's trace index.
63
+ def render_trace_page
64
+ limit, offset = page_params
65
+ members = Profiler.storage.find_by_trace(params[:trace_id].to_s).reverse
66
+ members = members.select { |p| p.profile_type == "http" } unless all_types?
67
+ render_page(members.drop(offset).first(limit + 1), limit, offset)
68
+ end
69
+
60
70
  # The children of a page, oldest last, read through the store's parent index.
61
71
  def render_children_page
62
72
  limit, offset = page_params
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ # GET /_profiler/health: the state of the circuit of each collector (see
5
+ # Profiler::Collectors::CircuitBreaker), as a page or, with a .json suffix or an Accept
6
+ # header, as JSON. Always 200: an open circuit means a collector is left out of the profiles,
7
+ # not that the profiler or the application is down, and a 503 would trip any probe pointed at it.
8
+ class HealthController < ApplicationController
9
+ def show
10
+ @enabled = Profiler.configuration.collector_circuit_breaker_enabled ? true : false
11
+ @threshold = Profiler.configuration.collector_circuit_breaker_threshold
12
+ @collectors = Profiler.configuration.collectors.map do |collector_class|
13
+ Collectors::CircuitBreaker.status(collector_class).merge(name: collector_class.name.to_s)
14
+ end
15
+
16
+ respond_to do |format|
17
+ format.html
18
+ format.json do
19
+ render json: {
20
+ circuit_breaker: { enabled: @enabled, threshold: @threshold },
21
+ collectors: @collectors
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ <div style="max-width: 900px; margin: 24px auto; padding: 0 16px; font-family: system-ui, sans-serif;">
2
+ <p><a href="<%= profiler.root_path %>">&larr; Profiler</a></p>
3
+ <h1>Collector health</h1>
4
+ <p>
5
+ Circuit breaker: <strong><%= @enabled ? "enabled" : "disabled" %></strong>,
6
+ opens after <strong><%= @threshold %></strong> consecutive failures.
7
+ </p>
8
+ <table style="border-collapse: collapse; width: 100%;">
9
+ <thead>
10
+ <tr>
11
+ <th style="text-align: left; border-bottom: 1px solid #888; padding: 6px;">Collector</th>
12
+ <th style="text-align: left; border-bottom: 1px solid #888; padding: 6px;">State</th>
13
+ <th style="text-align: right; border-bottom: 1px solid #888; padding: 6px;">Consecutive failures</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @collectors.each do |collector| %>
18
+ <tr>
19
+ <td style="padding: 6px;"><%= collector[:name] %></td>
20
+ <td style="padding: 6px; font-weight: bold; color: <%= { closed: "#2e7d32", open: "#c62828", half_open: "#ef6c00" }[collector[:state]] %>;"><%= collector[:state] %></td>
21
+ <td style="padding: 6px; text-align: right;"><%= collector[:failures] %></td>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ </table>
26
+ </div>
data/config/routes.rb CHANGED
@@ -29,6 +29,7 @@ Profiler::Engine.routes.draw do
29
29
  get "assets/icon.png", to: "assets#icon"
30
30
 
31
31
  get "test_runner", to: "test_runner#index"
32
+ get "health", to: "health#show"
32
33
 
33
34
  namespace :api do
34
35
  resources :profiles, only: [:index, :show, :destroy] do
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ module Collectors
5
+ # One circuit per collector class, shared by the threads and requests of the process. A
6
+ # class that fails config.collector_circuit_breaker_threshold times in a row (a subscribe or
7
+ # a collect that raises) is no longer instantiated: its circuit is open. After RECOVERY_SECONDS
8
+ # a single request tries it again (half open): a success closes the circuit, a failure
9
+ # opens it again for another RECOVERY_SECONDS. With the option off, nothing is counted and
10
+ # nothing is blocked.
11
+ module CircuitBreaker
12
+ RECOVERY_SECONDS = 60
13
+
14
+ # state: :closed, :open or :half_open. +since+ is when the circuit opened, or when the
15
+ # trial of a half open one was granted.
16
+ Circuit = Struct.new(:failures, :state, :since)
17
+
18
+ LOCK = Mutex.new
19
+ CIRCUITS = {}
20
+ private_constant :LOCK, :CIRCUITS
21
+
22
+ module_function
23
+
24
+ def enabled?
25
+ Profiler.configuration.collector_circuit_breaker_enabled ? true : false
26
+ end
27
+
28
+ # Whether a collector of this class is to be created for the next profile. An open circuit
29
+ # whose delay has passed answers true once, and becomes half open; a half open one answers
30
+ # false while its trial is under way, unless that trial never reported for RECOVERY_SECONDS.
31
+ def allow?(collector_class)
32
+ return true unless enabled?
33
+
34
+ LOCK.synchronize do
35
+ circuit = CIRCUITS[collector_class]
36
+ return true unless circuit
37
+
38
+ case circuit.state
39
+ when :closed then true
40
+ else
41
+ return false if now - circuit.since < RECOVERY_SECONDS
42
+
43
+ circuit.state = :half_open
44
+ circuit.since = now
45
+ true
46
+ end
47
+ end
48
+ end
49
+
50
+ def record_failure(collector_class)
51
+ return unless enabled?
52
+
53
+ LOCK.synchronize do
54
+ circuit = (CIRCUITS[collector_class] ||= Circuit.new(0, :closed, nil))
55
+ circuit.failures += 1
56
+ if circuit.state == :half_open || circuit.failures >= Profiler.configuration.collector_circuit_breaker_threshold
57
+ circuit.state = :open
58
+ circuit.since = now
59
+ end
60
+ end
61
+ end
62
+
63
+ def record_success(collector_class)
64
+ return unless enabled?
65
+
66
+ LOCK.synchronize do
67
+ circuit = CIRCUITS[collector_class]
68
+ return unless circuit
69
+
70
+ # A late success of a request that started before the circuit opened does not close it.
71
+ return if circuit.state == :open
72
+
73
+ CIRCUITS.delete(collector_class)
74
+ end
75
+ end
76
+
77
+ # { state:, failures: } for one class, without changing anything. An open circuit whose
78
+ # delay has passed is reported half open: the next request will try it.
79
+ def status(collector_class)
80
+ return { state: :closed, failures: 0 } unless enabled?
81
+
82
+ LOCK.synchronize do
83
+ circuit = CIRCUITS[collector_class]
84
+ return { state: :closed, failures: 0 } unless circuit
85
+
86
+ state = circuit.state
87
+ state = :half_open if state == :open && now - circuit.since >= RECOVERY_SECONDS
88
+ { state: state, failures: circuit.failures }
89
+ end
90
+ end
91
+
92
+ def reset!
93
+ LOCK.synchronize { CIRCUITS.clear }
94
+ end
95
+
96
+ # Monotonic seconds; a spec replaces it to move time.
97
+ def now
98
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "circuit_breaker"
4
+
3
5
  module Profiler
4
6
  module Collectors
5
7
  # Subscribes and releases the collectors of one profile, for every entry point that runs
@@ -7,14 +9,26 @@ module Profiler
7
9
  module Lifecycle
8
10
  module_function
9
11
 
10
- # Subscribes each collector in turn. When one fails, releases them all, the ones that
11
- # never subscribed included, and returns false: the caller then runs the work unprofiled.
12
+ # Subscribes each collector in turn. A collector whose subscribe raises is released and
13
+ # taken out of +collectors+ (the array is changed in place, so that the caller only collects
14
+ # the ones that subscribed); the others keep profiling. Returns false, with every collector
15
+ # released, only when none could subscribe: the caller then runs the work unprofiled.
12
16
  def subscribe_all(collectors, label)
13
- collectors.each { |collector| collector.subscribe if collector.respond_to?(:subscribe) }
17
+ return true if collectors.empty?
18
+
19
+ collectors.reject! { |collector| !subscribe_one(collector, label) }
20
+ !collectors.empty?
21
+ end
22
+
23
+ # Runs the block that collects from one collector. A failure is logged and counted against
24
+ # the collector's class (CircuitBreaker), a success clears that count; the others carry on.
25
+ def guard(collector, label)
26
+ yield
27
+ CircuitBreaker.record_success(collector.class)
14
28
  true
15
29
  rescue => e
16
- Profiler.log_error("#{label}: collector subscribe failed", e)
17
- release_all(collectors)
30
+ Profiler.log_error("#{label}: collector #{collector.class} failed", e)
31
+ CircuitBreaker.record_failure(collector.class)
18
32
  false
19
33
  end
20
34
 
@@ -26,6 +40,17 @@ module Profiler
26
40
  Profiler.log_error("collector #{collector.class} release failed", e)
27
41
  end
28
42
  end
43
+
44
+ def subscribe_one(collector, label)
45
+ collector.subscribe if collector.respond_to?(:subscribe)
46
+ true
47
+ rescue => e
48
+ Profiler.log_error("#{label}: collector #{collector.class} subscribe failed", e)
49
+ CircuitBreaker.record_failure(collector.class)
50
+ release_all([collector])
51
+ false
52
+ end
53
+ private_class_method :subscribe_one
29
54
  end
30
55
  end
31
56
  end
@@ -57,7 +57,8 @@ module Profiler
57
57
  :name, :master_url, :self_url,
58
58
  :cluster_heartbeat_interval, :cluster_offline_threshold,
59
59
  :cluster_master, :cluster_secret, :cluster_require_secret,
60
- :cluster_allow_insecure_http, :logger
60
+ :cluster_allow_insecure_http, :logger,
61
+ :collector_circuit_breaker_enabled
61
62
 
62
63
  attr_reader :authorize_block, :enabled, :track_tests, :max_profiles
63
64
 
@@ -156,6 +157,23 @@ module Profiler
156
157
  # Where the profiler's own errors go (Profiler.log): Rails.logger when nil, or $stderr when
157
158
  # there is none.
158
159
  @logger = nil
160
+ # A collector that fails this many times in a row is no longer run (see
161
+ # Profiler::Collectors::CircuitBreaker); false never stops one.
162
+ @collector_circuit_breaker_enabled = true
163
+ @collector_circuit_breaker_threshold = 3
164
+ end
165
+
166
+ attr_reader :collector_circuit_breaker_threshold
167
+
168
+ # A positive Integer. Anything else is left out with a warning, the previous value kept: the
169
+ # gem is loaded in production too, where a mistake here must not stop the application booting.
170
+ def collector_circuit_breaker_threshold=(value)
171
+ if value.is_a?(Integer) && value.positive?
172
+ @collector_circuit_breaker_threshold = value
173
+ else
174
+ Profiler.log_warn("config.collector_circuit_breaker_threshold: #{value.inspect} is not a positive " \
175
+ "Integer; keeping #{@collector_circuit_breaker_threshold}")
176
+ end
159
177
  end
160
178
 
161
179
  # Always a Pathname, whether set from a String or left to its default.
@@ -72,11 +72,9 @@ module Profiler
72
72
  profile.finish(console_status == "completed" ? 200 : 500)
73
73
 
74
74
  collectors.each do |collector|
75
- begin
75
+ Collectors::Lifecycle.guard(collector, "ConsoleProfiler") do
76
76
  collector.collect if collector.respond_to?(:collect)
77
77
  profile.add_collector_metadata(collector)
78
- rescue => e
79
- Profiler.log_error("ConsoleProfiler: collector #{collector.class} failed", e)
80
78
  end
81
79
  end
82
80
 
@@ -10,8 +10,18 @@ module Profiler
10
10
  Thread.current[:profiler_token] = value
11
11
  end
12
12
 
13
+ # The trace id of the profile in progress, for the outgoing calls to carry.
14
+ def self.trace_id
15
+ Thread.current[:profiler_trace_id]
16
+ end
17
+
18
+ def self.trace_id=(value)
19
+ Thread.current[:profiler_trace_id] = value
20
+ end
21
+
13
22
  def self.clear
14
23
  Thread.current[:profiler_token] = nil
24
+ Thread.current[:profiler_trace_id] = nil
15
25
  end
16
26
  end
17
27
  end
@@ -8,12 +8,18 @@ require "securerandom"
8
8
  require_relative "../redaction"
9
9
  require_relative "../body_content"
10
10
  require_relative "../configuration"
11
+ require_relative "../current_context"
12
+ require_relative "../trace_context"
11
13
 
12
14
  module Profiler
13
15
  module Instrumentation
14
16
  module NetHttpInstrumentation
15
17
  module RequestPatch
16
18
  def request(req, body = nil, &block)
19
+ # Before the guards below: the profiler's own calls (the master's requests to its
20
+ # slaves) carry the trace too, so that the slave's profile joins the request's trace.
21
+ NetHttpInstrumentation.inject_trace_context(req, address.to_s, port)
22
+
17
23
  collector = Thread.current[:profiler_http_collector]
18
24
  return super unless collector
19
25
  # Re-entrancy guard: Net::HTTP#request calls itself recursively when
@@ -128,6 +134,20 @@ module Profiler
128
134
  @installed = true
129
135
  end
130
136
 
137
+ # Adds a traceparent to a call made while a profiled request runs on this thread: the trace
138
+ # id of that request and a span id of its own. A traceparent the application set is kept,
139
+ # and a host excluded from the instrumentation gets none.
140
+ def self.inject_trace_context(req, host, port)
141
+ trace_id = CurrentContext.trace_id
142
+ return unless trace_id
143
+ return if req[TraceContext::HEADER]
144
+ return if skip_host?(host, port)
145
+
146
+ req[TraceContext::HEADER] = TraceContext.format(trace_id, TraceContext.new_span_id)
147
+ rescue => e
148
+ Profiler.log_error("NetHttpInstrumentation: could not add the traceparent header", e)
149
+ end
150
+
131
151
  # The hosts of http_skip_hosts, and the cluster's master (host and port): a slave's calls to
132
152
  # it are the profiler's, whichever code makes them.
133
153
  def self.skip_host?(host, port = nil)
@@ -102,11 +102,9 @@ module Profiler
102
102
  profile.finish(job_status == "completed" ? 200 : 500)
103
103
 
104
104
  collectors.each do |collector|
105
- begin
105
+ Collectors::Lifecycle.guard(collector, "JobProfiler") do
106
106
  collector.collect if collector.respond_to?(:collect)
107
107
  profile.add_collector_metadata(collector)
108
- rescue => e
109
- Profiler.log_error("JobProfiler: collector #{collector.class} failed", e)
110
108
  end
111
109
  end
112
110
 
@@ -3,6 +3,7 @@
3
3
  require "rack"
4
4
  require_relative "../models/profile"
5
5
  require_relative "../current_context"
6
+ require_relative "../trace_context"
6
7
  require_relative "../collectors/lifecycle"
7
8
  require_relative "toolbar_injector"
8
9
  require_relative "capturing_body"
@@ -31,6 +32,7 @@ module Profiler
31
32
  profile = Models::Profile.new(build_request(env), params: false)
32
33
  profile.gem_version = Profiler.version
33
34
  Profiler::CurrentContext.token = profile.token
35
+ assign_trace(env, profile)
34
36
 
35
37
  # Capture request body before app processes it
36
38
  request_body = read_request_body(env)
@@ -76,6 +78,16 @@ module Profiler
76
78
 
77
79
  private
78
80
 
81
+ # Continues the trace of a valid incoming traceparent (its parent-id becomes this profile's
82
+ # parent_span_id), or starts a new one. Either way the profile gets a span of its own.
83
+ def assign_trace(env, profile)
84
+ incoming = TraceContext.parse(env["HTTP_TRACEPARENT"])
85
+ profile.trace_id = incoming ? incoming.trace_id : TraceContext.new_trace_id
86
+ profile.parent_span_id = incoming&.parent_id
87
+ profile.span_id = TraceContext.new_span_id
88
+ Profiler::CurrentContext.trace_id = profile.trace_id
89
+ end
90
+
79
91
  # Read after the application has run, so after the routing: the route parameters are there,
80
92
  # and the params carry the encoding the route declared, as the controller saw them.
81
93
  def capture_params(env, profile)
@@ -153,10 +165,10 @@ module Profiler
153
165
  # A Rack 3 streaming body (call without each) writes to the socket itself: nothing to relay.
154
166
  unless body.respond_to?(:each)
155
167
  kept.each do |collector|
156
- collector.collect if collector.respond_to?(:collect)
157
- profile.refresh_collector_metadata(collector)
158
- rescue => e
159
- Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
168
+ Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
169
+ collector.collect if collector.respond_to?(:collect)
170
+ profile.refresh_collector_metadata(collector)
171
+ end
160
172
  end
161
173
  record_response(env, profile, request_body, status, profiled_headers, "", nil, false)
162
174
  # Saved before the headers leave: a profile that is not there gets no token.
@@ -199,10 +211,10 @@ module Profiler
199
211
  next unless collector.is_a?(Collectors::RequestCollector) ||
200
212
  (error && collector.respond_to?(:capture) && !collect_from_any_thread?(collector))
201
213
 
202
- collector.collect
203
- profile.refresh_collector_metadata(collector)
204
- rescue => e
205
- Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
214
+ Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
215
+ collector.collect
216
+ profile.refresh_collector_metadata(collector)
217
+ end
206
218
  end
207
219
 
208
220
  Profiler.save_profile(profile, from: "ProfilerMiddleware")
@@ -261,11 +273,9 @@ module Profiler
261
273
 
262
274
  def collect_all(profile, collectors)
263
275
  collectors.each do |collector|
264
- begin
276
+ Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
265
277
  collector.collect if collector.respond_to?(:collect)
266
278
  profile.add_collector_metadata(collector)
267
- rescue => e
268
- Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
269
279
  end
270
280
  end
271
281
  end
@@ -303,7 +313,9 @@ module Profiler
303
313
  # Builds the collectors without subscribing them: Lifecycle.subscribe_all does, so that
304
314
  # a failure part way leaves every collector reachable for release.
305
315
  def create_collectors(profile)
306
- Profiler.configuration.collectors.map { |collector_class| collector_class.new(profile) }
316
+ Profiler.configuration.collectors
317
+ .select { |collector_class| Collectors::CircuitBreaker.allow?(collector_class) }
318
+ .map { |collector_class| collector_class.new(profile) }
307
319
  end
308
320
 
309
321
  def html_response?(headers)
@@ -82,10 +82,10 @@ module Profiler
82
82
  @released = true
83
83
  @profile.collectors_released_after_seconds = after_seconds if after_seconds
84
84
  @collectors.each do |collector|
85
- collector.collect if collector.respond_to?(:collect)
86
- @profile.refresh_collector_metadata(collector)
87
- rescue => e
88
- Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
85
+ Collectors::Lifecycle.guard(collector, "ProfilerMiddleware") do
86
+ collector.collect if collector.respond_to?(:collect)
87
+ @profile.refresh_collector_metadata(collector)
88
+ end
89
89
  end
90
90
  Collectors::Lifecycle.release_all(@collectors)
91
91
  end
@@ -114,7 +114,9 @@ module Profiler
114
114
  # once the collectors are released.
115
115
  def enter
116
116
  token = CurrentContext.token
117
+ trace_id = CurrentContext.trace_id
117
118
  CurrentContext.token = @profile.token
119
+ CurrentContext.trace_id = @profile.trace_id
118
120
  lent = @mutex.synchronize do
119
121
  next [] if @released
120
122
 
@@ -122,13 +124,14 @@ module Profiler
122
124
  [collector, collector.lend_thread_slots] if collector.respond_to?(:lend_thread_slots)
123
125
  end
124
126
  end
125
- [token, lent]
127
+ [token, lent, trace_id]
126
128
  end
127
129
 
128
130
  def leave(state)
129
- token, lent = state
131
+ token, lent, trace_id = state
130
132
  lent.reverse_each { |collector, previous| collector.return_thread_slots(previous) }
131
133
  CurrentContext.token = token
134
+ CurrentContext.trace_id = trace_id
132
135
  end
133
136
  end
134
137
  end
@@ -21,7 +21,7 @@ module Profiler
21
21
  :request_body_size, :request_body_truncated, :request_body_size_is_minimum,
22
22
  :response_body_size, :response_body_truncated, :response_body_size_is_minimum,
23
23
  :collectors_released_after_seconds, :params_truncated,
24
- :gem_version
24
+ :gem_version, :trace_id, :span_id, :parent_span_id
25
25
 
26
26
  # +params+: false leaves the params to capture_params, for a caller that builds the profile
27
27
  # before the router has run.
@@ -167,6 +167,9 @@ module Profiler
167
167
  collectors_data: @collectors_data,
168
168
  tabs: @collectors_metadata,
169
169
  parent_token: @parent_token,
170
+ trace_id: @trace_id,
171
+ span_id: @span_id,
172
+ parent_span_id: @parent_span_id,
170
173
  is_ajax: @is_ajax
171
174
  }
172
175
  end
@@ -217,6 +220,9 @@ module Profiler
217
220
  profile.is_ajax = data[:is_ajax] || false
218
221
  profile.profile_type = data[:profile_type] || "http"
219
222
  profile.gem_version = data[:gem_version]
223
+ profile.trace_id = data[:trace_id]
224
+ profile.span_id = data[:span_id]
225
+ profile.parent_span_id = data[:parent_span_id]
220
226
 
221
227
  # Convert collectors_data keys to strings recursively for consistency
222
228
  profile.collectors_data = (data[:collectors_data] || {}).transform_keys(&:to_s).transform_values do |value|
@@ -45,6 +45,11 @@ module Profiler
45
45
  raise NotImplementedError, "#{self.class} must implement #find_by_parent"
46
46
  end
47
47
 
48
+ # The profiles sharing a trace id, oldest first.
49
+ def find_by_trace(trace_id)
50
+ raise NotImplementedError, "#{self.class} must implement #find_by_trace"
51
+ end
52
+
48
53
  def delete(token)
49
54
  raise NotImplementedError, "#{self.class} must implement #delete"
50
55
  end
@@ -41,7 +41,7 @@ module Profiler
41
41
  # Past a cap, the oldest profiles are evicted down to this share of it.
42
42
  LOW_WATER = 0.8
43
43
 
44
- Entry = Struct.new(:token, :at, :type, :parent, :bytes, :summary, :sequence)
44
+ Entry = Struct.new(:token, :at, :type, :parent, :bytes, :summary, :sequence, :trace)
45
45
 
46
46
  def initialize(options = {})
47
47
  super()
@@ -138,6 +138,16 @@ module Profiler
138
138
  children.filter_map { |entry| load(entry.token) }.sort_by(&:started_at)
139
139
  end
140
140
 
141
+ def find_by_trace(trace_id)
142
+ return [] unless Token.valid?(trace_id)
143
+
144
+ members = @mutex.synchronize do
145
+ refresh
146
+ (@traces[trace_id] || {}).keys.filter_map { |token| @entries[token] }
147
+ end
148
+ members.filter_map { |entry| load(entry.token) }.sort_by(&:started_at)
149
+ end
150
+
141
151
  def delete(token)
142
152
  return unless Token.valid?(token)
143
153
 
@@ -173,13 +183,14 @@ module Profiler
173
183
  def put_record(token, data, bytes, started_at: nil)
174
184
  started_at = started_at&.to_f || (data[:started_at] ? Time.parse(data[:started_at]).to_f : Time.now.to_f)
175
185
  { "op" => "put", "token" => token, "at" => started_at, "type" => (data[:profile_type] || "http").to_s,
176
- "parent" => data[:parent_token], "bytes" => bytes, "summary" => Summary.build(data) }
186
+ "parent" => data[:parent_token], "trace" => data[:trace_id], "bytes" => bytes, "summary" => Summary.build(data) }
177
187
  end
178
188
 
179
189
  def forget_index
180
190
  @entries = {}
181
191
  @sequence = 0
182
192
  @children = {}
193
+ @traces = {}
183
194
  @bytes = 0
184
195
  @records = 0
185
196
  @offset = 0
@@ -243,8 +254,9 @@ module Profiler
243
254
  when "put"
244
255
  remove_entry(token)
245
256
  parent = Token.valid?(record["parent"]) ? record["parent"] : nil
257
+ trace = Token.valid?(record["trace"]) ? record["trace"] : nil
246
258
  add_entry(Entry.new(token, record["at"].to_f, record["type"], parent, record["bytes"].to_i,
247
- record["summary"].is_a?(Hash) ? record["summary"] : nil))
259
+ record["summary"].is_a?(Hash) ? record["summary"] : nil, nil, trace))
248
260
  when "del"
249
261
  remove_entry(token)
250
262
  end
@@ -255,6 +267,7 @@ module Profiler
255
267
  @entries[entry.token] = entry
256
268
  @bytes += entry.bytes
257
269
  (@children[entry.parent] ||= {})[entry.token] = true if entry.parent
270
+ (@traces[entry.trace] ||= {})[entry.token] = true if entry.trace
258
271
  @sorted = nil
259
272
  end
260
273
 
@@ -267,6 +280,10 @@ module Profiler
267
280
  siblings.delete(token)
268
281
  @children.delete(entry.parent) if siblings.empty?
269
282
  end
283
+ if entry.trace && (members = @traces[entry.trace])
284
+ members.delete(token)
285
+ @traces.delete(entry.trace) if members.empty?
286
+ end
270
287
  @sorted = nil
271
288
  end
272
289
 
@@ -344,7 +361,8 @@ module Profiler
344
361
  def entry_from_file(token)
345
362
  json = PrivateFiles.open_for_reading(profile_file_path(token), &:read)
346
363
  record = put_record(token, Models::Profile.from_json(json).to_h, json.bytesize)
347
- Entry.new(token, record["at"], record["type"], record["parent"], record["bytes"], record["summary"])
364
+ trace = Token.valid?(record["trace"]) ? record["trace"] : nil
365
+ Entry.new(token, record["at"], record["type"], record["parent"], record["bytes"], record["summary"], nil, trace)
348
366
  rescue StandardError => e
349
367
  # Unreadable: kept in the count, so that it is evicted in turn; listed by no one.
350
368
  Profiler.log_error_once(:file_store_entry, "FileStore: could not read profile #{token} to index it", e)
@@ -353,7 +371,7 @@ module Profiler
353
371
  rescue SystemCallError
354
372
  nil
355
373
  end
356
- Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil)
374
+ Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil, nil, nil)
357
375
  end
358
376
 
359
377
  def modified_at(token)
@@ -390,7 +408,8 @@ module Profiler
390
408
  "compacted_at" => Time.now.to_f)]
391
409
  @entries.each_value do |entry|
392
410
  lines << JSON.generate("op" => "put", "token" => entry.token, "at" => entry.at, "type" => entry.type,
393
- "parent" => entry.parent, "bytes" => entry.bytes, "summary" => entry.summary)
411
+ "parent" => entry.parent, "trace" => entry.trace, "bytes" => entry.bytes,
412
+ "summary" => entry.summary)
394
413
  end
395
414
  data = "#{lines.join("\n")}\n"
396
415
  PrivateFiles.write(@index_path, data)