Class: Upkeep::Runtime::Recorder

Inherits:
Object
  • Object
show all
Defined in:
lib/upkeep/runtime.rb

Defined Under Namespace

Classes: RefusedBoundary

Constant Summary collapse

REQUEST_NODE_ID =
:request

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(graph: nil, profile: false) ⇒ Recorder

Returns a new instance of Recorder.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/upkeep/runtime.rb', line 82

def initialize(graph: nil, profile: false)
  @frame_stack = []
  @graph = graph || DAG::Graph.new
  @profile = profile
  @profile_timings = Hash.new(0.0)
  @profile_counts = Hash.new(0)
  @refused_boundaries = []
  @ambient_replay_inputs_by_owner = Hash.new do |owners, owner_id|
    owners[owner_id] = Hash.new { |sources, source| sources[source] = {} }
  end
  @pending_dependencies_by_owner = Hash.new { |owners, owner_id| owners[owner_id] = {} }
  @relation_provenance_by_collection_id = {}
  @graph.add_node(REQUEST_NODE_ID, kind: :request, payload: {}) unless @graph.node?(REQUEST_NODE_ID)
  @subscription_shape_trace = DAG::SubscriptionShape::Trace.new(graph_version: @graph.version)
end

Instance Attribute Details

#graphObject (readonly)

Returns the value of attribute graph.



80
81
82
# File 'lib/upkeep/runtime.rb', line 80

def graph
  @graph
end

#refused_boundariesObject (readonly)

Returns the value of attribute refused_boundaries.



80
81
82
# File 'lib/upkeep/runtime.rb', line 80

def refused_boundaries
  @refused_boundaries
end

Class Method Details

.from_h(snapshot) ⇒ Object



98
99
100
101
# File 'lib/upkeep/runtime.rb', line 98

def self.from_h(snapshot)
  snapshot = Dependencies.symbolize_keys(snapshot)
  new(graph: DAG::Graph.from_h(snapshot.fetch(:graph)))
end

Instance Method Details

#ambient_replay_inputs_for(owner_id) ⇒ Object



182
183
184
185
186
# File 'lib/upkeep/runtime.rb', line 182

def ambient_replay_inputs_for(owner_id)
  @ambient_replay_inputs_by_owner.fetch(owner_id, {}).each_with_object({}) do |(source, values), inputs|
    inputs[source] = values.dup
  end
end

#current_frameObject



230
231
232
# File 'lib/upkeep/runtime.rb', line 230

def current_frame
  @frame_stack.last
end

#current_ownerObject



234
235
236
# File 'lib/upkeep/runtime.rb', line 234

def current_owner
  current_frame || REQUEST_NODE_ID
end

#flush_pending_dependencies(owner_id = nil) ⇒ Object



150
151
152
153
154
155
156
157
158
# File 'lib/upkeep/runtime.rb', line 150

def flush_pending_dependencies(owner_id = nil)
  if owner_id
    flush_pending_dependencies_for(owner_id)
  else
    @pending_dependencies_by_owner.keys.each { |pending_owner_id| flush_pending_dependencies_for(pending_owner_id) }
  end
ensure
  @pending_dependencies_by_owner.delete(owner_id) if owner_id
end

#identity_profile(frame_id) ⇒ Object



238
239
240
241
# File 'lib/upkeep/runtime.rb', line 238

def identity_profile(frame_id)
  flush_pending_dependencies
  identity_dependencies_for(frame_id).map(&:to_h)
end

#identity_signature(frame_id) ⇒ Object



243
244
245
246
247
248
249
# File 'lib/upkeep/runtime.rb', line 243

def identity_signature(frame_id)
  flush_pending_dependencies
  identity_dependencies = identity_dependencies_for(frame_id)
  return "public" if identity_dependencies.empty?

  Digest::SHA256.hexdigest(identity_dependencies.map(&:identity_key).sort_by(&:inspect).inspect)[0, 16]
end

#profile_countsObject



222
223
224
# File 'lib/upkeep/runtime.rb', line 222

def profile_counts
  @profile_counts.dup
end

#profile_timingsObject



218
219
220
# File 'lib/upkeep/runtime.rb', line 218

def profile_timings
  @profile_timings.transform_values { |value| value.round(3) }
end

#reactive?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'lib/upkeep/runtime.rb', line 226

def reactive?
  @refused_boundaries.empty?
end

#record_ambient_replay_input(source, key, value) ⇒ Object



175
176
177
178
179
180
# File 'lib/upkeep/runtime.rb', line 175

def record_ambient_replay_input(source, key, value)
  profile_count(:recorder_ambient_replay_input_count)
  profile_timing(:recorder_ambient_replay_input_ms) do
    @ambient_replay_inputs_by_owner[current_owner][source.to_sym][key.to_s] = value
  end
end

#record_dependency(dependency) ⇒ Object



160
161
162
163
164
165
166
# File 'lib/upkeep/runtime.rb', line 160

def record_dependency(dependency)
  profile_count(:recorder_dependency_count)
  profile_timing(:recorder_dependency_ms) do
    owner_id = current_owner
    @pending_dependencies_by_owner[owner_id][dependency.cache_key] ||= dependency
  end
end

#record_relation_provenance(collection, model_name:, analysis:) ⇒ Object



188
189
190
191
192
193
194
195
196
# File 'lib/upkeep/runtime.rb', line 188

def record_relation_provenance(collection, model_name:, analysis:)
  return unless collection && analysis

  profile_count(:recorder_relation_provenance_count)
  profile_timing(:recorder_relation_provenance_ms) do
    @relation_provenance_by_collection_id[collection.object_id] =
      RelationProvenance.new(model_name.to_s, analysis)
  end
end

#refuse_boundary(reason:, message:, suggestions:, source:) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/upkeep/runtime.rb', line 202

def refuse_boundary(reason:, message:, suggestions:, source:)
  profile_count(:recorder_refused_boundary_count)
  profile_timing(:recorder_refused_boundary_ms) do
    boundary = RefusedBoundary.new(
      reason.to_s,
      message.to_s,
      Array(suggestions).map(&:to_s),
      source.to_s
    )
    return false if @refused_boundaries.include?(boundary)

    @refused_boundaries << boundary
    true
  end
end

#relation_provenance_for(collection) ⇒ Object



198
199
200
# File 'lib/upkeep/runtime.rb', line 198

def relation_provenance_for(collection)
  @relation_provenance_by_collection_id[collection.object_id] if collection
end

#replace_scope(scope_id, replacement) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/upkeep/runtime.rb', line 103

def replace_scope(scope_id, replacement)
  replacement.graph.dependencies_for(REQUEST_NODE_ID).each do |dependency|
    replacement.graph.add_dependency(scope_id, dependency)
  end

  composed = if graph.node?(scope_id)
    graph.replace_subgraph(scope_id, replacement.graph)
  else
    graph.attach_subgraph(
      scope_id,
      replacement.graph,
      parent_id: REQUEST_NODE_ID
    )
  end
  self.class.new(graph: composed)
end

#subscription_shape(request_signature: nil) ⇒ Object



168
169
170
171
172
173
# File 'lib/upkeep/runtime.rb', line 168

def subscription_shape(request_signature: nil)
  flush_pending_dependencies
  return @subscription_shape_trace.subscription_shape(request_signature: request_signature) if @subscription_shape_trace.covers?(@graph)

  DAG::SubscriptionShape.from_graph(@graph, request_signature: request_signature)
end

#to_h(dependencies: :all) ⇒ Object



120
121
122
123
# File 'lib/upkeep/runtime.rb', line 120

def to_h(dependencies: :all)
  flush_pending_dependencies
  { graph: graph.to_h(dependencies: dependencies) }
end

#to_persistent_hObject



125
126
127
# File 'lib/upkeep/runtime.rb', line 125

def to_persistent_h
  to_h(dependencies: :identity)
end

#with_frame(frame_id, metadata) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/upkeep/runtime.rb', line 129

def with_frame(frame_id, )
  profile_count(:recorder_frame_count)
  parent_id = nil
  profile_timing(:recorder_frame_ms) do
    invalidate_subscription_shape_trace_if_needed
    parent_id = current_owner
    @graph.add_node(frame_id, kind: :frame, payload: )
    @graph.add_edge(parent_id, frame_id, reason: :contains)
    profile_timing(:recorder_shape_trace_ms) do
      @subscription_shape_trace.record_frame(frame_id, , parent_id: parent_id, graph_version: @graph.version)
    end
  end
  @frame_stack.push(frame_id)
  begin
    yield
  ensure
    flush_pending_dependencies(frame_id)
    @frame_stack.pop
  end
end