trailblazer-developer 0.0.28 → 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 +4 -4
- data/CHANGES.md +75 -0
- data/Gemfile +2 -0
- data/lib/trailblazer/developer/debugger/normalizer.rb +60 -0
- data/lib/trailblazer/developer/debugger.rb +97 -0
- data/lib/trailblazer/developer/trace/node.rb +103 -0
- data/lib/trailblazer/developer/trace/parent_map.rb +32 -0
- data/lib/trailblazer/developer/trace/present.rb +50 -18
- data/lib/trailblazer/developer/trace/snapshot/value.rb +39 -0
- data/lib/trailblazer/developer/trace/snapshot/versions.rb +105 -0
- data/lib/trailblazer/developer/trace/snapshot.rb +71 -0
- data/lib/trailblazer/developer/trace/stack.rb +18 -5
- data/lib/trailblazer/developer/trace.rb +34 -59
- data/lib/trailblazer/developer/version.rb +1 -1
- data/lib/trailblazer/developer/wtf/renderer.rb +8 -8
- data/lib/trailblazer/developer/wtf.rb +47 -47
- data/lib/trailblazer/developer.rb +8 -3
- metadata +9 -5
- data/lib/trailblazer/developer/trace/debugger/normalizer.rb +0 -68
- data/lib/trailblazer/developer/trace/debugger.rb +0 -72
- data/lib/trailblazer/developer/trace/tree.rb +0 -86
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 343ca726bbb8da50f05aca2a552f018ac67683d4c0021226f828a30132ecf85c
|
4
|
+
data.tar.gz: f4ab036e3da016ab3283f258c749ef98cee474a9c69b8303fad36bdd9b6df945
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f66fa880eb4a0673fd881598fe9fa9addebadbd4989fded9366f7960b7ed9ab34794fe8d8a6c4ab3a315b13fe45b048061ac4f4330cb71942a82a3adc1d12a82
|
7
|
+
data.tar.gz: 7ecac119ec1d406442e1ddb5292639bac41e13df24d870f462065d90d5bf690ab4417b0736c9a134b51c2d2929a6952912803cfe2686cfca7d7b16bc14cfaa4a
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,78 @@
|
|
1
|
+
# 0.1.0
|
2
|
+
|
3
|
+
* Improvement of tracing performance: factor ~4.
|
4
|
+
* Consistent interface for tracing and presentation.
|
5
|
+
|
6
|
+
## Adding `Debugger` layer
|
7
|
+
|
8
|
+
* Introduce `Debugger::Trace` with `Trace::Node`s and `variable_versions` field
|
9
|
+
to maintain all data produced by tracing in one entity.
|
10
|
+
* In `Trace::Present.call`, you need to pass a block with options for customization instead
|
11
|
+
of passing `:node_options`. Further on, the per-node customization is now keyed by
|
12
|
+
`Trace::Node` instance and not a Stack element anymore.
|
13
|
+
* In `render_method`, remove `:tree` keyword argument, use `:debugger_trace`.
|
14
|
+
* The `render_method` is called with keyword arguments, only. The first positional argument
|
15
|
+
`debugger_nodes` is now `:debugger_trace`.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
output = Dev::Trace::Present.(
|
19
|
+
stack,
|
20
|
+
node_options: {
|
21
|
+
stack.to_a[0] => {label: "Create"}
|
22
|
+
}
|
23
|
+
)
|
24
|
+
```
|
25
|
+
is now
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
Dev::Trace::Present.(stack) do |trace_nodes:, **|
|
29
|
+
{
|
30
|
+
node_options: {
|
31
|
+
trace_nodes[0] => {label: "Create"}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## `Trace::Node`
|
38
|
+
|
39
|
+
* `Node.build_for_stack` is now called in `Trace::Present` and produces a list
|
40
|
+
of `Trace::Node` instances that are containers for matching before and after snapshots.
|
41
|
+
* Add `Node::Incomplete` nodes that have no `snapshot_after` as they represent
|
42
|
+
a part of the flow that was canceled.
|
43
|
+
|
44
|
+
## Debugger::Normalizer
|
45
|
+
|
46
|
+
* Deprecate `:captured_node` keyword argument in normalizer steps and
|
47
|
+
rename it to `:trace_node`.
|
48
|
+
* Remove the concept of `:runtime_path` and `:compile_path`. You can always
|
49
|
+
compute paths after or during the debug rendering yourself. However, we simply
|
50
|
+
don't need it for IDE or wtf?.
|
51
|
+
|
52
|
+
## `Trace.wtf?`
|
53
|
+
|
54
|
+
* Rename `:output_data_collector` and `:input_data_collector` to `:after_snapshooter` and `:before_snapshooter`
|
55
|
+
as they both produce instances of `Snapshot`. The return signature has changed, the snapshooters
|
56
|
+
return two values now: the data object and an object representing the variable versioning.
|
57
|
+
|
58
|
+
## Snapshot
|
59
|
+
|
60
|
+
* Introduce the concept of `Snapshot` which can be a moment in time before or after a step.
|
61
|
+
The `Snapshot::Before` and `Snapshot::After` (renamed from `Captured::Input` and `Captured::Output`)
|
62
|
+
are created during the tracing and after tracing associated to a specific `Trace::Node`.
|
63
|
+
* Add `Snapshot::Ctx` and `Snapshot::Versions` which is a new, faster way of capturing variables.
|
64
|
+
Instead of always calling `ctx.inspect` for every trace step, only variables that have changed
|
65
|
+
are snapshooted using the (configurable) `:value_snapshooter`. This improves `#wtf?` performance
|
66
|
+
up to factor 10.
|
67
|
+
|
68
|
+
# 0.0.29
|
69
|
+
|
70
|
+
* The `:render_method` callable can now return output along with additional returned values.
|
71
|
+
* Pass the top-level `:activity` into the `:render_method`.
|
72
|
+
* In `#wtf?`, only complete the stack when an exception occurred.
|
73
|
+
* `wtf?` now returns all original return values plus the computed output from `:render_method`,
|
74
|
+
followed by an arbitrary object from the rendering code.
|
75
|
+
|
1
76
|
# 0.0.28
|
2
77
|
|
3
78
|
* Move `Introspect::Graph` over from `trailblazer-activity`. It's a data structure very specific
|
data/Gemfile
CHANGED
@@ -0,0 +1,60 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module Developer
|
3
|
+
module Debugger
|
4
|
+
# @private
|
5
|
+
# Public entry point to add Debugger::Node normalizer steps.
|
6
|
+
def self.add_normalizer_step!(step, id:, normalizer: Normalizer::PIPELINES.last, **options)
|
7
|
+
task = Normalizer.Task(step)
|
8
|
+
|
9
|
+
# We have a TaskWrap::Pipeline (a very simple style of "activity" used for normalizers) and
|
10
|
+
# add another step using the "friendly interface" from {Activity::Adds}.
|
11
|
+
options = {append: nil} unless options.any?
|
12
|
+
|
13
|
+
pipeline_extension = Activity::TaskWrap::Extension.build([task, id: id, **options])
|
14
|
+
|
15
|
+
Normalizer::PIPELINES << pipeline_extension.(normalizer)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run at runtime when preparing a Trace::Nodes for presentation.
|
19
|
+
module Normalizer
|
20
|
+
def self.Task(user_step) # TODO: we could keep this in the {activity} gem.
|
21
|
+
Activity::TaskWrap::Pipeline::TaskAdapter.for_step(user_step, option: false) # we don't need Option as we don't have ciruit_options here, and no {:exec_context}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Default steps for the Debugger::Node options pipeline, following the step-interface.
|
25
|
+
module Default
|
26
|
+
def self.compile_id(ctx, activity:, task:, **)
|
27
|
+
ctx[:compile_id] = Activity::Introspect.Nodes(activity, task: task)[:id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.runtime_id(ctx, compile_id:, **)
|
31
|
+
ctx[:runtime_id] = compile_id
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.label(ctx, label: nil, runtime_id:, **)
|
35
|
+
ctx[:label] = label || runtime_id
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.data(ctx, data: {}, **)
|
39
|
+
ctx[:data] = data
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.incomplete?(ctx, trace_node:, **)
|
43
|
+
ctx[:incomplete?] = trace_node.is_a?(Developer::Trace::Node::Incomplete)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
default_steps = {
|
48
|
+
compile_id: Normalizer.Task(Default.method(:compile_id)),
|
49
|
+
runtime_id: Normalizer.Task(Default.method(:runtime_id)),
|
50
|
+
label: Normalizer.Task(Default.method(:label)),
|
51
|
+
data: Normalizer.Task(Default.method(:data)),
|
52
|
+
incomplete?: Normalizer.Task(Default.method(:incomplete?)),
|
53
|
+
}.
|
54
|
+
collect { |id, task| Activity::TaskWrap::Pipeline.Row(id, task) }
|
55
|
+
|
56
|
+
PIPELINES = [Activity::TaskWrap::Pipeline.new(default_steps)] # we do mutate this constant at compile-time. Maybe # DISCUSS and find a better way.
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module Developer
|
3
|
+
# Code in Debugger is only executed if the user wants to render the stack.
|
4
|
+
module Debugger
|
5
|
+
ATTRS = [
|
6
|
+
:id,
|
7
|
+
:trace_node,
|
8
|
+
:task,
|
9
|
+
:activity,
|
10
|
+
:compile_id,
|
11
|
+
:runtime_id,
|
12
|
+
:label,
|
13
|
+
:data,
|
14
|
+
:snapshot_before,
|
15
|
+
:snapshot_after,
|
16
|
+
:level,
|
17
|
+
:incomplete?,
|
18
|
+
:captured_node, # TODO: remove once macro is 2.2
|
19
|
+
]
|
20
|
+
|
21
|
+
# The {Debugger::Node} is an abstraction between Trace::Node and the actual rendering layer (why?)
|
22
|
+
#
|
23
|
+
# TODO: class, "type",
|
24
|
+
# which track, return signal, etc
|
25
|
+
class Node < Struct.new(*ATTRS, keyword_init: true)
|
26
|
+
# we always key options for specific nodes by Stack::Captured::Input, so we don't confuse activities if they were called multiple times.
|
27
|
+
#
|
28
|
+
# @return [Debugger::Node] array of Debugger::Node
|
29
|
+
def self.build(trace_nodes, node_options: {}, normalizer: Debugger::Normalizer::PIPELINES.last, **options_for_nodes)
|
30
|
+
# DISCUSS: this might change if we introduce a new Node type for Trace.
|
31
|
+
_debugger_nodes = trace_nodes.collect do |trace_node|
|
32
|
+
# it's possible to pass per-node options, like {label: "Yo!"} via {:node_options[<snapshot_before>]}
|
33
|
+
options_from_user = node_options[trace_node] || {}
|
34
|
+
|
35
|
+
options_from_trace_node = trace_node
|
36
|
+
.to_h # :level, :snapshot_before, :snapshot_after
|
37
|
+
.merge(
|
38
|
+
id: trace_node.object_id,
|
39
|
+
trace_node: trace_node,
|
40
|
+
activity: trace_node.snapshot_before.activity,
|
41
|
+
task: trace_node.task,
|
42
|
+
captured_node: DeprecatedCapturedNode, # TODO: remove once macro is 2.2
|
43
|
+
)
|
44
|
+
|
45
|
+
options_for_debugger_node, _ = normalizer.(
|
46
|
+
{
|
47
|
+
**options_from_trace_node,
|
48
|
+
**options_from_user
|
49
|
+
},
|
50
|
+
[]
|
51
|
+
)
|
52
|
+
|
53
|
+
# these attributes are not changing with the presentation
|
54
|
+
Debugger::Node.new(**options_for_debugger_node).freeze
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO: remove once macro is 2.2
|
59
|
+
class DeprecatedCapturedNode
|
60
|
+
def self.method_missing(*)
|
61
|
+
raise "[Trailblazer] The `:captured_node` argument is deprecated, please upgrade to `trailblazer-developer-0.1.0` and use `:trace_node` if the upgrade doesn't fix it."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end # Node
|
65
|
+
|
66
|
+
# Interface for data (nodes, versions, etc) between tracing code and presentation layer.
|
67
|
+
# We have no concept of {Stack} here anymore. Nodes and arbitrary objects such as "versions".
|
68
|
+
# Debugger::Trace interface abstracts away the fact we have two snapshots. Here,
|
69
|
+
# we only have a node per task.
|
70
|
+
#
|
71
|
+
class Trace
|
72
|
+
# Called in {Trace::Present}.
|
73
|
+
# Design note: goal here is to have as little computation as possible.
|
74
|
+
def self.build(stack, trace_nodes, **options_for_debugger_nodes)
|
75
|
+
nodes = Debugger::Node.build(
|
76
|
+
trace_nodes,
|
77
|
+
**options_for_debugger_nodes,
|
78
|
+
)
|
79
|
+
|
80
|
+
new(nodes: nodes, variable_versions: stack.variable_versions) # after this, the concept of "Stack" doesn't exist anymore.
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(nodes:, variable_versions:)
|
84
|
+
@options = {nodes: nodes, variable_versions: variable_versions}
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_h
|
88
|
+
@options
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_a
|
92
|
+
to_h[:nodes].to_a
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end # Debugger
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module Developer
|
3
|
+
module Trace
|
4
|
+
# Build array of {Trace::Node} from a snapshots stack.
|
5
|
+
# @private
|
6
|
+
def self.build_nodes(snapshots)
|
7
|
+
instructions = [
|
8
|
+
[0, snapshots]
|
9
|
+
]
|
10
|
+
|
11
|
+
_nodes = Node.process_instructions(instructions)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Datastructure representing a trace.
|
15
|
+
class Node < Struct.new(:level, :task, :snapshot_before, :snapshot_after)
|
16
|
+
class Incomplete < Node
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.pop_from_instructions!(instructions)
|
20
|
+
while (level, remaining_snapshots = instructions.pop)
|
21
|
+
next if level.nil?
|
22
|
+
next if remaining_snapshots.empty?
|
23
|
+
|
24
|
+
return level, remaining_snapshots
|
25
|
+
end
|
26
|
+
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
# def self.BLA(instructions)
|
31
|
+
# instructions.collect do |(level, remaining_snapshots)|
|
32
|
+
# [
|
33
|
+
# level,
|
34
|
+
# remaining_snapshots.collect { |snap| [snap.class, snap.task] }
|
35
|
+
# ]
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
|
39
|
+
def self.process_instructions(instructions) # FIXME: mutating argument
|
40
|
+
nodes = []
|
41
|
+
|
42
|
+
while (level, remaining_snapshots = pop_from_instructions!(instructions))
|
43
|
+
raise unless remaining_snapshots[0].is_a?(Snapshot::Before) # DISCUSS: remove assertion?
|
44
|
+
|
45
|
+
node, new_instructions = node_and_instructions_for(remaining_snapshots[0], remaining_snapshots[1..-1], level: level)
|
46
|
+
# pp BLA(new_instructions)
|
47
|
+
|
48
|
+
nodes << node
|
49
|
+
|
50
|
+
instructions += new_instructions
|
51
|
+
end
|
52
|
+
|
53
|
+
return nodes
|
54
|
+
end
|
55
|
+
|
56
|
+
# Called per snapshot_before "process_branch"
|
57
|
+
# 1. Find, for snapshot_before, the matching snapshot_after in the stack
|
58
|
+
# 2. Extract snapshots inbetween those two. These are min. 1 level deeper in!
|
59
|
+
# 3. Run process_siblings for 2.
|
60
|
+
def self.node_and_instructions_for(snapshot_before, descendants, level:)
|
61
|
+
# Find closing snapshot for this branch.
|
62
|
+
snapshot_after = descendants.find do |snapshot|
|
63
|
+
snapshot.is_a?(Snapshot::After) && snapshot.data[:snapshot_before] == snapshot_before
|
64
|
+
end
|
65
|
+
|
66
|
+
if snapshot_after
|
67
|
+
snapshot_after_index = descendants.index(snapshot_after)
|
68
|
+
|
69
|
+
instructions =
|
70
|
+
if snapshot_after_index == 0 # E.g. before/Start, after/Start
|
71
|
+
[
|
72
|
+
[level, descendants[1..-1]]
|
73
|
+
]
|
74
|
+
else
|
75
|
+
nested_instructions = [
|
76
|
+
# instruction to go through the remaining, behind this tuple.
|
77
|
+
[
|
78
|
+
level,
|
79
|
+
descendants[(snapshot_after_index + 1)..-1]
|
80
|
+
],
|
81
|
+
# instruction to go through all snapshots between this current tuple.
|
82
|
+
[
|
83
|
+
level + 1,
|
84
|
+
descendants[0..snapshot_after_index - 1], # "new descendants"
|
85
|
+
],
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
node = new(level, snapshot_before.task, snapshot_before, snapshot_after)
|
90
|
+
else # incomplete
|
91
|
+
instructions = [
|
92
|
+
[level + 1, descendants]
|
93
|
+
]
|
94
|
+
|
95
|
+
node = Incomplete.new(level, snapshot_before.task, snapshot_before, nil)
|
96
|
+
end
|
97
|
+
|
98
|
+
return node, instructions
|
99
|
+
end
|
100
|
+
end # Node
|
101
|
+
end
|
102
|
+
end # Developer
|
103
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module Developer
|
3
|
+
module Trace
|
4
|
+
# Map each {Node} instance to its parent {Node}.
|
5
|
+
module ParentMap # DISCUSS: where does this belong?
|
6
|
+
def self.build(trace_nodes)
|
7
|
+
levels = {}
|
8
|
+
trace_nodes.collect do |node|
|
9
|
+
level = node.level
|
10
|
+
levels[level] = node
|
11
|
+
|
12
|
+
[node, levels[level - 1]]
|
13
|
+
end.to_h
|
14
|
+
end
|
15
|
+
|
16
|
+
# @public
|
17
|
+
def self.path_for(parent_map, node)
|
18
|
+
path = []
|
19
|
+
|
20
|
+
while parent = parent_map[node] # DISCUSS: what if the graphs are cached and present, already?
|
21
|
+
node_id = Activity::Introspect.Nodes(node.snapshot_before.activity, task: node.snapshot_before.task).id
|
22
|
+
path << node_id
|
23
|
+
|
24
|
+
node = parent
|
25
|
+
end
|
26
|
+
|
27
|
+
path.reverse
|
28
|
+
end
|
29
|
+
end # ParentMap
|
30
|
+
end
|
31
|
+
end # Developer
|
32
|
+
end
|
@@ -2,7 +2,7 @@ require "hirb"
|
|
2
2
|
|
3
3
|
module Trailblazer::Developer
|
4
4
|
module Trace
|
5
|
-
module Present
|
5
|
+
module Present # DISCUSS: rename to Debugger?
|
6
6
|
module_function
|
7
7
|
|
8
8
|
# @private
|
@@ -12,33 +12,65 @@ module Trailblazer::Developer
|
|
12
12
|
|
13
13
|
# Returns the console output string.
|
14
14
|
# @private
|
15
|
-
def render(
|
16
|
-
nodes =
|
17
|
-
renderer.(debugger_node: debugger_node,
|
15
|
+
def render(debugger_trace:, renderer: method(:default_renderer), **options_for_renderer)
|
16
|
+
nodes = debugger_trace.to_a.collect do |debugger_node|
|
17
|
+
renderer.(debugger_node: debugger_node, debugger_trace: debugger_trace, **options_for_renderer)
|
18
18
|
end
|
19
19
|
|
20
20
|
Hirb::Console.format_output(nodes, class: :tree, type: :directory, multi_line_nodes: true)
|
21
21
|
end
|
22
22
|
|
23
|
-
# Entry point for rendering a Stack
|
24
|
-
|
23
|
+
# Entry point for rendering a {Trace::Stack}.
|
24
|
+
# Used in `#wtf?`.
|
25
|
+
def call(stack, render_method: method(:render), **options, &block)
|
26
|
+
deprecate_node_options!(**options) # TODO: remove in 0.2.0.
|
27
|
+
|
28
|
+
# Build a generic array of {Trace::Node}s.
|
29
|
+
trace_nodes = Trace.build_nodes(stack.to_a)
|
30
|
+
|
25
31
|
# The top activity doesn't have an ID, hence we need to compute a default label.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
top_activity_trace_node = trace_nodes[0]
|
33
|
+
|
34
|
+
build_options = {
|
35
|
+
node_options: {
|
36
|
+
# we can pass particular label "hints".
|
37
|
+
top_activity_trace_node => {
|
38
|
+
# label: %{#{top_activity_trace_node.task.superclass} (anonymous)},
|
39
|
+
label: top_activity_trace_node.task.inspect,
|
40
|
+
},
|
41
|
+
}
|
35
42
|
}
|
36
43
|
|
37
|
-
|
44
|
+
build_options = build_options.merge(options) # since we only have {:node_options} in {build_options}, we can safely merge here.
|
45
|
+
|
46
|
+
# specific rendering.
|
47
|
+
options_from_block = block_given? ? block.call(trace_nodes: trace_nodes, stack: stack, **build_options) : {}
|
48
|
+
|
49
|
+
build_options = merge_local_options(options_from_block, build_options)
|
50
|
+
|
51
|
+
# currently, we agree on using a Debugger::Node list as the presentation data structure.
|
52
|
+
debugger_trace = Debugger::Trace.build(stack, trace_nodes, **build_options)
|
38
53
|
|
39
|
-
|
54
|
+
return render_method.(debugger_trace: debugger_trace, **build_options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def deprecate_node_options!(node_options: nil, **) # TODO: remove in 0.2.0.
|
58
|
+
return unless node_options
|
59
|
+
|
60
|
+
raise "[Trailblazer] The `:node_options` option for `Trace::Present` is deprecated.
|
61
|
+
Please use the block style as described here: https://trailblazer.to/2.1/docs/internals.html#internals-developer-trace-present"
|
62
|
+
end
|
63
|
+
|
64
|
+
# @private
|
65
|
+
def merge_local_options(options, local_options)
|
66
|
+
merged_hash = options.collect do |key, value|
|
67
|
+
[
|
68
|
+
key,
|
69
|
+
value.is_a?(Hash) ? local_options.fetch(key, {}).merge(value) : value # options are winning over local_options[key]
|
70
|
+
]
|
71
|
+
end.to_h
|
40
72
|
|
41
|
-
|
73
|
+
local_options.merge(merged_hash)
|
42
74
|
end
|
43
75
|
end # Present
|
44
76
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Trailblazer::Developer
|
2
|
+
module Trace
|
3
|
+
class Snapshot
|
4
|
+
# {Value} serializes the variable value using with custom logic, e.g. {value.inspect}.
|
5
|
+
# A series of matchers decide which snapshooter is used.
|
6
|
+
class Value
|
7
|
+
def initialize(matchers)
|
8
|
+
@matchers = matchers
|
9
|
+
end
|
10
|
+
|
11
|
+
# DISCUSS: this could be a compiled pattern matching `case/in` block here.
|
12
|
+
def call(name, value, **options)
|
13
|
+
@matchers.each do |matcher, inspect_method|
|
14
|
+
if matcher.(name, value, **options)
|
15
|
+
return inspect_method.(name, value, **options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
raise "no matcher found for #{name.inspect}" # TODO: this should never happen.
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.default_variable_inspect(name, value, ctx:)
|
23
|
+
value.inspect
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build
|
27
|
+
new(
|
28
|
+
[
|
29
|
+
[
|
30
|
+
->(*) { true }, # matches everything
|
31
|
+
method(:default_variable_inspect)
|
32
|
+
]
|
33
|
+
]
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Trailblazer::Developer
|
2
|
+
module Trace
|
3
|
+
class Snapshot
|
4
|
+
# Snapshot::Ctx keeps an inspected version of each ctx variable.
|
5
|
+
# We figure out if a variable has changed by using `variable.hash` (works
|
6
|
+
# even with deeply nested structures).
|
7
|
+
#
|
8
|
+
# Key idea here is to have minimum work at operation-runtime. Specifics like
|
9
|
+
# figuring out what has changed can be done when using the debugger.
|
10
|
+
#
|
11
|
+
# By keeping "old" versions, we get three benefits.
|
12
|
+
# 1. We only need to call {inspect} once on a traced variable. Especially
|
13
|
+
# when variables are complex structures or strings, this dramatically speeds
|
14
|
+
# up tracing, from same-ish to factor 5!
|
15
|
+
# 2. The content sent to our debugger is much smaller which reduces network load
|
16
|
+
# and storage space.
|
17
|
+
# 3. Presentation becomes simpler as we "know" what variable has changed.
|
18
|
+
#
|
19
|
+
# Possible problems: when {variable.hash} returns the same key even though the
|
20
|
+
# data has changed.
|
21
|
+
#
|
22
|
+
# DISCUSS: speed up by checking mutable, only?
|
23
|
+
# DISCUSS: we currently only use this for testing.
|
24
|
+
# DISCUSS: this has knowledge about {Stack} internals.
|
25
|
+
#
|
26
|
+
# This is for the "rendering" layer.
|
27
|
+
# @private
|
28
|
+
def self.snapshot_ctx_for(snapshot, variable_versions)
|
29
|
+
variable_versions = variable_versions.instance_variable_get(:@variables)
|
30
|
+
|
31
|
+
snapshot.data[:ctx_variable_changeset].collect do |name, hash, has_changed|
|
32
|
+
[
|
33
|
+
name,
|
34
|
+
{
|
35
|
+
value: variable_versions[name][hash],
|
36
|
+
has_changed: !!has_changed,
|
37
|
+
}
|
38
|
+
]
|
39
|
+
end.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
# A table of all ctx variables, their hashes and serialized values.
|
43
|
+
#
|
44
|
+
# {:current_user=>
|
45
|
+
# {3298051090906520533=>"#<TraceTest::User:0x000055b2e3424460 @id=1>",
|
46
|
+
# 3764938782671692590=>"#<TraceTest::User:0x000055b2e33e45b8 @id=2>"},
|
47
|
+
# :params=>
|
48
|
+
# {2911818769466875657=>"{:name=>\"Q & I\"}",
|
49
|
+
# 2238394858183550663=>"{:name=>\"Q & I\", :song=>{...}}"},
|
50
|
+
# :seq=>
|
51
|
+
# {-105020188158523405=>"[]",
|
52
|
+
# -2281497291400788995=>"[:authenticate]",
|
53
|
+
# 150926802063554866=>"[:authenticate, :authorize]",
|
54
|
+
# 3339595138798116233=>"[:authenticate, :authorize, :model]",
|
55
|
+
# -3395325862879242711=>
|
56
|
+
# "[:authenticate, :authorize, :model, :screw_params!]"},
|
57
|
+
# :model=>{348183403054247453=>"Object"}}
|
58
|
+
class Versions
|
59
|
+
def initialize
|
60
|
+
@variables = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# DISCUSS: problem with changeset is, we have to go through variables twice.
|
64
|
+
def changeset_for(ctx, value_snapshooter:)
|
65
|
+
new_versions = []
|
66
|
+
|
67
|
+
changeset_for_snapshot = ctx.collect do |name, value|
|
68
|
+
# DISCUSS: do we have to call that explicitly or does Hash#[] do that for us, anyway?
|
69
|
+
value_hash = value.hash # DISCUSS: does this really always change when a deeply nested object changes?
|
70
|
+
|
71
|
+
if (variable_versions = @variables[name]) && variable_versions.key?(value_hash) # TODO: test {variable: nil} value
|
72
|
+
[name, value_hash, nil] # nil means it's an existing reference.
|
73
|
+
else
|
74
|
+
value_snapshot = value_snapshooter.(name, value, ctx: ctx)
|
75
|
+
|
76
|
+
version = [name, value_hash, value_snapshot]
|
77
|
+
|
78
|
+
new_versions << version
|
79
|
+
version
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
return changeset_for_snapshot, new_versions
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_changes!(new_versions)
|
87
|
+
new_versions.each do |args|
|
88
|
+
add_variable_version!(*args)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @private
|
93
|
+
def add_variable_version!(name, hash, value)
|
94
|
+
@variables[name] ||= {}
|
95
|
+
|
96
|
+
@variables[name][hash] = value # i hate mutations.
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_h
|
100
|
+
@variables
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end # Snapshot
|
104
|
+
end
|
105
|
+
end
|