trailblazer-developer 0.0.28 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|