trailblazer-developer 0.0.27 → 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/.github/workflows/ci.yml +1 -4
- data/CHANGES.md +84 -0
- data/Gemfile +10 -5
- data/lib/trailblazer/developer/debugger/normalizer.rb +60 -0
- data/lib/trailblazer/developer/debugger.rb +97 -0
- data/lib/trailblazer/developer/introspect/graph.rb +83 -0
- data/lib/trailblazer/developer/render/circuit.rb +6 -54
- data/lib/trailblazer/developer/render/linear.rb +1 -1
- data/lib/trailblazer/developer/render/task_wrap.rb +1 -1
- 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 +9 -3
- data/trailblazer-developer.gemspec +2 -3
- metadata +16 -14
- data/.github/workflows/ci_jruby.yml +0 -19
- data/.github/workflows/ci_legacy.yml +0 -19
- data/.github/workflows/ci_truffleruby.yml +0 -19
- data/lib/trailblazer/developer/trace/debugger/normalizer.rb +0 -68
- data/lib/trailblazer/developer/trace/debugger.rb +0 -81
- data/lib/trailblazer/developer/trace/tree.rb +0 -86
@@ -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
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Trailblazer::Developer
|
2
|
+
module Trace
|
3
|
+
# WARNING: the interfaces here are subject to change, we're still experimenting
|
4
|
+
# with the architecture of tracing, and a healthy balance of performance/memory
|
5
|
+
# and clean design.
|
6
|
+
# A Snapshot comprises of data captured before of after a "step". This usually
|
7
|
+
# includes a ctx snapshot, variable versions and a returned signal for after-step
|
8
|
+
# snapshots.
|
9
|
+
#
|
10
|
+
# Note that {Before} and {After} are generic concepts know to Trace::Present and Debugger.
|
11
|
+
#
|
12
|
+
# Snapshot::After{
|
13
|
+
# signal: <End.Success>
|
14
|
+
# ctx_snapshot: Snapshot::Ctx{
|
15
|
+
# variable_versions: [:current_user, 0], [:model, 0]
|
16
|
+
# }
|
17
|
+
# }
|
18
|
+
class Snapshot < Struct.new(:task, :activity, :data)
|
19
|
+
Before = Class.new(Snapshot)
|
20
|
+
After = Class.new(Snapshot)
|
21
|
+
|
22
|
+
# This is called from {Trace.capture_args} and {Trace.capture_return} in the taskWrap.
|
23
|
+
def self.call(ctx_snapshooter, wrap_config, ((ctx, flow_options), circuit_options))
|
24
|
+
# DISCUSS: grab the {snapshooter} here from flow_options, instead of in Trace.capture_args?
|
25
|
+
changeset, new_versions = ctx_snapshooter.call(wrap_config, [[ctx, flow_options], circuit_options])
|
26
|
+
|
27
|
+
snapshot = new( # either Before or After.
|
28
|
+
wrap_config[:task],
|
29
|
+
circuit_options[:activity],
|
30
|
+
changeset
|
31
|
+
).freeze
|
32
|
+
|
33
|
+
return snapshot, new_versions
|
34
|
+
end
|
35
|
+
|
36
|
+
# Serialize all ctx variables before {call_task}.
|
37
|
+
# This is run just before {call_task}, after In().
|
38
|
+
def self.before_snapshooter(wrap_ctx, ((ctx, flow_options), _))
|
39
|
+
changeset, new_versions = snapshot_for(ctx, **flow_options)
|
40
|
+
|
41
|
+
data = {
|
42
|
+
ctx_variable_changeset: changeset,
|
43
|
+
}
|
44
|
+
|
45
|
+
return data, new_versions
|
46
|
+
end
|
47
|
+
|
48
|
+
# Serialize all ctx variables at the very end of taskWrap, after Out().
|
49
|
+
def self.after_snapshooter(wrap_ctx, _)
|
50
|
+
snapshot_before = wrap_ctx[:snapshot_before]
|
51
|
+
returned_ctx, flow_options = wrap_ctx[:return_args]
|
52
|
+
|
53
|
+
changeset, new_versions = snapshot_for(returned_ctx, **flow_options)
|
54
|
+
|
55
|
+
data = {
|
56
|
+
ctx_variable_changeset: changeset,
|
57
|
+
signal: wrap_ctx[:return_signal],
|
58
|
+
snapshot_before: snapshot_before, # add this so we know who belongs together.
|
59
|
+
}
|
60
|
+
|
61
|
+
return data, new_versions
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.snapshot_for(ctx, value_snapshooter:, stack:, **)
|
65
|
+
variable_versions = stack.variable_versions
|
66
|
+
|
67
|
+
variable_versions.changeset_for(ctx, value_snapshooter: value_snapshooter) # return {changeset, new_versions}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -3,17 +3,30 @@ module Trailblazer
|
|
3
3
|
module Trace
|
4
4
|
# The stack is a linear one-dimensional array. Per traced task two {Trace::Captured}
|
5
5
|
# elements get pushed onto it (unless there's an Exception).
|
6
|
+
#
|
7
|
+
# The Stack object maintains the snapshots and the variable versions. It should probably
|
8
|
+
# be named "Trace" :D
|
9
|
+
# It is by design coupled to both Snapshot and Ctx::Versions.
|
6
10
|
class Stack
|
7
|
-
def initialize(
|
8
|
-
@
|
11
|
+
def initialize(snapshots = [], variable_versions = Snapshot::Versions.new)
|
12
|
+
@snapshots = snapshots
|
13
|
+
@variable_versions = variable_versions # DISCUSS: I dislike the coupling here to Stack, but introducting another object comprised of Stack and VariableVersions seems overkill.
|
9
14
|
end
|
10
15
|
|
11
|
-
|
12
|
-
|
16
|
+
attr_reader :variable_versions # TODO: the accessor sucks. But I guess to_h[:variable_versions] is slower.
|
17
|
+
|
18
|
+
def add!(snapshot, new_variable_versions)
|
19
|
+
# variable_versions is mutated in the snapshooter, that's
|
20
|
+
# why we don't have to re-set it here. I'm not a huge fan of mutating it
|
21
|
+
# in a deeply nested scenario but everything else we played with added huge amounts
|
22
|
+
# or runtime code.
|
23
|
+
@variable_versions.add_changes!(new_variable_versions)
|
24
|
+
|
25
|
+
@snapshots << snapshot
|
13
26
|
end
|
14
27
|
|
15
28
|
def to_a
|
16
|
-
@
|
29
|
+
@snapshots
|
17
30
|
end
|
18
31
|
end # Stack
|
19
32
|
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module Trailblazer::Developer
|
2
2
|
module Trace
|
3
|
-
|
4
3
|
class << self
|
5
|
-
# Public entry point to
|
4
|
+
# Public entry point to run an activity with tracing.
|
5
|
+
# It returns the accumulated stack of Snapshots, along with the original return values.
|
6
|
+
# Note that {Trace.invoke} does not do any rendering.
|
6
7
|
def call(activity, (ctx, flow_options), **circuit_options)
|
7
|
-
activity, (ctx, flow_options), circuit_options = Trace.arguments_for_call(
|
8
|
+
activity, (ctx, flow_options), circuit_options = Trace.arguments_for_call(activity, [ctx, flow_options], **circuit_options) # only run once for the entire circuit!
|
8
9
|
|
9
10
|
signal, (ctx, flow_options) = Trailblazer::Activity::TaskWrap.invoke(activity, [ctx, flow_options], **circuit_options)
|
10
11
|
|
@@ -15,9 +16,10 @@ module Trailblazer::Developer
|
|
15
16
|
|
16
17
|
def arguments_for_call(activity, (options, original_flow_options), **original_circuit_options)
|
17
18
|
default_flow_options = {
|
18
|
-
stack:
|
19
|
-
|
20
|
-
|
19
|
+
stack: Trace::Stack.new,
|
20
|
+
before_snapshooter: Snapshot.method(:before_snapshooter),
|
21
|
+
after_snapshooter: Snapshot.method(:after_snapshooter),
|
22
|
+
value_snapshooter: Trace.value_snapshooter
|
21
23
|
}
|
22
24
|
|
23
25
|
flow_options = {**default_flow_options, **Hash(original_flow_options)}
|
@@ -32,79 +34,52 @@ module Trailblazer::Developer
|
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
37
|
+
@value_snapshooter = Trace::Snapshot::Value.build()
|
38
|
+
singleton_class.attr_reader :value_snapshooter # NOTE: this is semi-private.
|
39
|
+
|
35
40
|
module_function
|
36
|
-
|
37
|
-
# and before the TaskWrap is finished.
|
38
|
-
#
|
41
|
+
|
39
42
|
# @private
|
40
43
|
def task_wrap_extensions
|
41
|
-
|
42
|
-
[Trace.method(:capture_args), id: "task_wrap.capture_args", prepend: "task_wrap.call_task"],
|
43
|
-
[Trace.method(:capture_return), id: "task_wrap.capture_return", append: nil], # append to the very end of tW.
|
44
|
-
)
|
44
|
+
TASK_WRAP_EXTENSION
|
45
45
|
end
|
46
46
|
|
47
|
+
# Snapshot::Before and After are a generic concept of Trace, as
|
48
|
+
# they're the interface to Trace::Present, WTF, and Debugger.
|
49
|
+
|
47
50
|
# It's important to understand that {flow[:stack]} is mutated by design. This is needed so
|
48
51
|
# in case of exceptions we still have a "global" trace - unfortunately Ruby doesn't allow
|
49
52
|
# us a better way.
|
50
53
|
# taskWrap step to capture incoming arguments of a step.
|
51
|
-
|
52
|
-
|
54
|
+
#
|
55
|
+
# Note that we save the created {Snapshot::Before} in the wrap_ctx.
|
56
|
+
def capture_args(wrap_config, original_args)
|
57
|
+
flow_options = original_args[0][1]
|
53
58
|
|
54
|
-
|
59
|
+
snapshot, new_versions = Snapshot::Before.(flow_options[:before_snapshooter], wrap_config, original_args)
|
55
60
|
|
56
|
-
|
61
|
+
# We try to be generic here in the taskWrap snapshooting code, where details happen in Snapshot::Before/After and Stack#add!.
|
62
|
+
flow_options[:stack].add!(snapshot, new_versions)
|
57
63
|
|
58
|
-
return wrap_config, original_args
|
64
|
+
return wrap_config.merge(snapshot_before: snapshot), original_args
|
59
65
|
end
|
60
66
|
|
61
67
|
# taskWrap step to capture outgoing arguments from a step.
|
62
|
-
def capture_return(wrap_config, ((ctx,
|
63
|
-
original_args = [[ctx,
|
68
|
+
def capture_return(wrap_config, ((ctx, flow_options), circuit_options))
|
69
|
+
original_args = [[ctx, flow_options], circuit_options]
|
64
70
|
|
65
|
-
|
71
|
+
snapshot, new_versions = Snapshot::After.(flow_options[:after_snapshooter], wrap_config, original_args)
|
66
72
|
|
67
|
-
|
73
|
+
flow_options[:stack].add!(snapshot, new_versions)
|
68
74
|
|
69
75
|
return wrap_config, original_args
|
70
76
|
end
|
71
77
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
collected_data
|
79
|
-
).freeze
|
80
|
-
end
|
81
|
-
|
82
|
-
# Called in {#Captured}.
|
83
|
-
# DISCUSS: this is where to start for a new {Inspector} implementation.
|
84
|
-
def default_input_data_collector(wrap_config, ((ctx, _), _)) # DISCUSS: would it be faster to access ctx via {original_args[0][0]}?
|
85
|
-
# mutable, old_ctx = ctx.decompose
|
86
|
-
# mutable, old_ctx = ctx, nil
|
87
|
-
|
88
|
-
{
|
89
|
-
# ctx: ctx.to_h.freeze,
|
90
|
-
ctx_snapshot: ctx.to_h.collect { |k,v| [k, v.inspect] }.to_h,
|
91
|
-
} # TODO: proper snapshot!
|
92
|
-
end
|
93
|
-
|
94
|
-
# Called in {#Captured}.
|
95
|
-
def default_output_data_collector(wrap_config, ((ctx, _), _))
|
96
|
-
returned_ctx, _ = wrap_config[:return_args]
|
97
|
-
|
98
|
-
# FIXME: snapshot!
|
99
|
-
{
|
100
|
-
# ctx: ctx.to_h.freeze,
|
101
|
-
ctx_snapshot: returned_ctx.to_h.collect { |k,v| [k, v.inspect] }.to_h,
|
102
|
-
signal: wrap_config[:return_signal]
|
103
|
-
}
|
104
|
-
end
|
105
|
-
|
106
|
-
Captured = Struct.new(:task, :activity, :data)
|
107
|
-
Captured::Input = Class.new(Captured)
|
108
|
-
Captured::Output = Class.new(Captured)
|
78
|
+
# Insertions for the trace tasks that capture the arguments just before calling the task,
|
79
|
+
# and before the TaskWrap is finished.
|
80
|
+
TASK_WRAP_EXTENSION = Trailblazer::Activity::TaskWrap.Extension(
|
81
|
+
[Trace.method(:capture_args), id: "task_wrap.capture_args", prepend: "task_wrap.call_task"],
|
82
|
+
[Trace.method(:capture_return), id: "task_wrap.capture_return", append: nil], # append to the very end of tW.
|
83
|
+
)
|
109
84
|
end
|
110
85
|
end
|
@@ -14,21 +14,21 @@ module Trailblazer::Developer
|
|
14
14
|
|
15
15
|
module_function
|
16
16
|
|
17
|
-
# {options} can be {style: {#<
|
18
|
-
def call(
|
19
|
-
label = styled_label(
|
17
|
+
# {options} can be {style: {#<Debugger::Node> => [:red, :bold]}}
|
18
|
+
def call(debugger_trace:, debugger_node:, style: {}, **options)
|
19
|
+
label = styled_label(debugger_trace, debugger_node, style: style, **options)
|
20
20
|
|
21
21
|
[debugger_node.level, label]
|
22
22
|
end
|
23
23
|
|
24
|
-
def styled_label(
|
24
|
+
def styled_label(debugger_trace, debugger_node, color_map:, **options)
|
25
25
|
label = apply_style(debugger_node.label, debugger_node, **options)
|
26
26
|
|
27
|
-
%{#{fmt(label, color_map[
|
27
|
+
%{#{fmt(label, color_map[signal_of(debugger_node)])}}
|
28
28
|
end
|
29
29
|
|
30
30
|
def apply_style(label, debugger_node, style:, **)
|
31
|
-
return label unless styles = style[debugger_node.
|
31
|
+
return label unless styles = style[debugger_node.trace_node]
|
32
32
|
|
33
33
|
styles.each { |s| label = fmt(label, s) }
|
34
34
|
label
|
@@ -42,8 +42,8 @@ module Trailblazer::Developer
|
|
42
42
|
String.send(style, line)
|
43
43
|
end
|
44
44
|
|
45
|
-
def signal_of(
|
46
|
-
entity_signal =
|
45
|
+
def signal_of(debugger_node)
|
46
|
+
entity_signal = debugger_node.incomplete? ? nil : debugger_node.snapshot_after.data[:signal]
|
47
47
|
entity_klass = entity_signal.is_a?(Class) ? entity_signal : entity_signal.class
|
48
48
|
|
49
49
|
SIGNALS_MAP[entity_klass.name.to_sym]
|
@@ -16,62 +16,62 @@ module Trailblazer::Developer
|
|
16
16
|
# This allows to display the trace even when an exception happened
|
17
17
|
def invoke(activity, (ctx, flow_options), present_options: {}, **circuit_options)
|
18
18
|
flow_options ||= {}
|
19
|
+
local_present_options_block = ->(*) { {} }
|
19
20
|
|
20
21
|
stack = Trace::Stack.new # unfortunately, we need this mutable object before things break.
|
22
|
+
raise_exception = false
|
23
|
+
|
24
|
+
begin
|
25
|
+
complete_stack, signal, (ctx, flow_options) = Trace.invoke(
|
26
|
+
activity,
|
27
|
+
[ctx, flow_options.merge(stack: stack)],
|
28
|
+
**circuit_options
|
29
|
+
)
|
30
|
+
rescue
|
31
|
+
raise_exception = $! # TODO: will this show the very same stacktrace?
|
32
|
+
|
33
|
+
exception_source = Exception.find_exception_source(stack, $!)
|
34
|
+
complete_stack = stack
|
35
|
+
|
36
|
+
local_present_options_block = ->(trace_nodes:, **) {
|
37
|
+
exception_source_node = trace_nodes.reverse.find do |trace_node|
|
38
|
+
trace_node.snapshot_after == exception_source || trace_node.snapshot_before == exception_source
|
39
|
+
end
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# However, what if an error happens in, say, an input filter? TODO: test this
|
37
|
-
|
38
|
-
complete_stack = Exception::Stack.complete(incomplete_stack) # TODO: only in case of exception!
|
39
|
-
|
40
|
-
puts Trace::Present.(
|
41
|
+
{
|
42
|
+
# we can hand in options per node, identified by their captured_input part.
|
43
|
+
node_options: {
|
44
|
+
exception_source_node => {data: {exception_source: true}}, # goes to {Debugger::Node.build}
|
45
|
+
},
|
46
|
+
style: {
|
47
|
+
exception_source_node => [:red, :bold]
|
48
|
+
},
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# always render the trace.
|
54
|
+
output, returned_args = Trace::Present.(
|
41
55
|
complete_stack,
|
42
|
-
# we can hand in options per node, identified by their captured_input part.
|
43
|
-
node_options: {
|
44
|
-
exception_source => {data: {exception_source: true}}, # goes to {Debugger::Node.build}
|
45
|
-
},
|
46
|
-
|
47
56
|
renderer: Wtf::Renderer,
|
48
|
-
color_map: Wtf::Renderer::DEFAULT_COLOR_MAP.merge(
|
49
|
-
|
50
|
-
**present_options,
|
57
|
+
color_map: Wtf::Renderer::DEFAULT_COLOR_MAP.merge(flow_options[:color_map] || {}),
|
58
|
+
activity: activity,
|
59
|
+
**present_options,
|
60
|
+
&local_present_options_block
|
51
61
|
)
|
52
|
-
end
|
53
|
-
|
54
|
-
module Exception
|
55
|
-
# When an exception occurs the Stack instance is incomplete - it is missing Captured::Output instances
|
56
|
-
# for Inputs still open. This method adds the missing elements so the Trace::Tree algorithm doesn't crash.
|
57
|
-
module Stack
|
58
|
-
def self.complete(incomplete_stack)
|
59
|
-
processed = []
|
60
62
|
|
61
|
-
|
62
|
-
if captured.is_a?(Trace::Captured::Input)
|
63
|
-
processed << captured
|
64
|
-
else
|
65
|
-
processed.pop
|
66
|
-
end
|
67
|
-
end
|
63
|
+
puts output
|
68
64
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
73
|
-
end # Stack
|
65
|
+
raise raise_exception if raise_exception
|
66
|
+
return signal, [ctx, flow_options], circuit_options, output, returned_args
|
67
|
+
end
|
74
68
|
|
69
|
+
module Exception
|
70
|
+
def self.find_exception_source(stack, exception)
|
71
|
+
# in 99%, exception_source is a {Snapshot::Before}.
|
72
|
+
exception_source = stack.to_a.last # DISCUSS: in most cases, this is where the problem has happened.
|
73
|
+
# However, what if an error happens in, say, an input filter? TODO: test this
|
74
|
+
end
|
75
75
|
end
|
76
76
|
end
|
77
77
|
end
|
@@ -9,13 +9,19 @@ end
|
|
9
9
|
require "trailblazer/activity"
|
10
10
|
require "trailblazer/developer/wtf"
|
11
11
|
require "trailblazer/developer/wtf/renderer"
|
12
|
+
require "trailblazer/developer/trace/snapshot"
|
13
|
+
require "trailblazer/developer/trace/snapshot/value"
|
14
|
+
require "trailblazer/developer/trace/snapshot/versions"
|
12
15
|
require "trailblazer/developer/trace"
|
13
16
|
require "trailblazer/developer/trace/stack"
|
14
|
-
require "trailblazer/developer/trace/
|
17
|
+
require "trailblazer/developer/trace/node"
|
18
|
+
require "trailblazer/developer/trace/parent_map"
|
15
19
|
require "trailblazer/developer/trace/present"
|
16
|
-
require "trailblazer/developer/
|
20
|
+
require "trailblazer/developer/debugger"
|
17
21
|
require "trailblazer/developer/render/circuit"
|
18
22
|
require "trailblazer/developer/render/linear"
|
19
23
|
require "trailblazer/developer/render/task_wrap"
|
20
24
|
require "trailblazer/developer/introspect" # TODO: might get removed, again.
|
21
|
-
require "trailblazer/developer/
|
25
|
+
require "trailblazer/developer/debugger/normalizer"
|
26
|
+
require "trailblazer/developer/introspect/graph"
|
27
|
+
Trailblazer::Developer::Trace::Debugger = Trailblazer::Developer::Debugger # FIXME: deprecate constant!
|