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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45cd9fac3c0701cd0e98573e27c59c80de31d5d91b306980c961d58f64161d71
4
- data.tar.gz: 745d4d28834261571fdbccc9949b597bb1f17f669bc238a6bbc09925001d4594
3
+ metadata.gz: 343ca726bbb8da50f05aca2a552f018ac67683d4c0021226f828a30132ecf85c
4
+ data.tar.gz: f4ab036e3da016ab3283f258c749ef98cee474a9c69b8303fad36bdd9b6df945
5
5
  SHA512:
6
- metadata.gz: 57bdf494ed795a80edadba06bcc011d9ee5832eb6a4f87f0c02a5a1b0a81b82be05507eb4eb593cd6476b71efb7f9a1a82eced117dcc5e41175d028f5c2c8f0e
7
- data.tar.gz: 6735f1eea27c3fa7c87d394d3d0a9baf9c2218f632f8bd2d2b5b9325366a3d5ff47a5e89668bd8146429e8ef00ad814b9593fa0e8fbea7519dbe3dd345ae14bd
6
+ metadata.gz: f66fa880eb4a0673fd881598fe9fa9addebadbd4989fded9366f7960b7ed9ab34794fe8d8a6c4ab3a315b13fe45b048061ac4f4330cb71942a82a3adc1d12a82
7
+ data.tar.gz: 7ecac119ec1d406442e1ddb5292639bac41e13df24d870f462065d90d5bf690ab4417b0736c9a134b51c2d2929a6952912803cfe2686cfca7d7b16bc14cfaa4a
@@ -1,6 +1,3 @@
1
- ## This file is managed by Terraform.
2
- ## Do not modify this file directly, as it may be overwritten.
3
- ## Please open an issue instead.
4
1
  name: CI
5
2
  on: [push, pull_request]
6
3
  jobs:
@@ -8,7 +5,7 @@ jobs:
8
5
  strategy:
9
6
  fail-fast: false
10
7
  matrix:
11
- ruby: [2.7, '3.0', '3.1']
8
+ ruby: [2.6, 2.7, '3.0', '3.1', '3.2', 'jruby']
12
9
  runs-on: ubuntu-latest
13
10
  steps:
14
11
  - uses: actions/checkout@v3
data/CHANGES.md CHANGED
@@ -1,3 +1,87 @@
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
+
76
+ # 0.0.28
77
+
78
+ * Move `Introspect::Graph` over from `trailblazer-activity`. It's a data structure very specific
79
+ to rendering, which is not a part of pure runtime behavior.
80
+ * Use `Introspect.Nodes` API instead of `TaskMap`.
81
+ * Remove `:task_maps_per_activity` in `Debugger` as we don't need to cache anymore.
82
+ * Require `trailblazer-activity-dsl-linear-1.2.0`.
83
+ * Remove `Render::Circuit` as this is part of `trailblazer-activity` now.
84
+
1
85
  # 0.0.27
2
86
 
3
87
  ## Trace
data/Gemfile CHANGED
@@ -3,8 +3,13 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in trailblazer-developer.gemspec
4
4
  gemspec
5
5
 
6
- # gem "trailblazer-activity", ">= 0.7.1"
7
- # gem "trailblazer-activity", path: "../trailblazer-activity"
8
- # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
9
- gem "trailblazer-activity-dsl-linear", github: "trailblazer/trailblazer-activity-dsl-linear"
10
- gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
6
+ # gem "trailblazer-activity", path: "../trailblazer-activity"
7
+ # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
8
+ # gem "trailblazer-operation", path: "../trailblazer-operation"
9
+ # gem "trailblazer-pro", path: "../trailblazer-pro"
10
+
11
+ # gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
12
+ # gem "trailblazer-activity-dsl-linear", github: "trailblazer/trailblazer-activity-dsl-linear"
13
+
14
+
15
+ gem "benchmark-ips"
@@ -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,83 @@
1
+ # NOTE: The Graph API might get deprecated and replaced.
2
+ module Trailblazer
3
+ module Developer
4
+ module Introspect
5
+ # TODO: order of step/fail/pass in Node would be cool to have
6
+
7
+ # TODO: Remove Graph. This is only useful to render the full circuit
8
+ # Some thoughts here:
9
+ # * where do we need Schema.outputs? and where task.outputs?
10
+ #
11
+ #
12
+ # @private This API is still under construction.
13
+ class Graph
14
+ def initialize(activity)
15
+ @schema = activity.to_h or raise
16
+ @circuit = @schema[:circuit]
17
+ @map = @circuit.to_h[:map]
18
+ @configs = @schema[:nodes]
19
+ end
20
+
21
+ def find(id = nil, &block)
22
+ return find_by_id(id) unless block_given?
23
+
24
+ find_with_block(&block)
25
+ end
26
+
27
+ # TODO: convert to {#to_a}.
28
+ def collect(strategy: :circuit)
29
+ @map.keys.each_with_index.collect { |task, i| yield find_with_block { |node| node.task == task }, i }
30
+ end
31
+
32
+ def stop_events
33
+ @circuit.to_h[:end_events]
34
+ end
35
+
36
+ private
37
+
38
+ def find_by_id(id)
39
+ node = @configs.find { |_, _node| _node.id == id } or return
40
+ node_for(node[1])
41
+ end
42
+
43
+ def find_with_block
44
+ existing = @configs.find { |_, node| yield Node(node.task, node.id, node.outputs, node.data) } or return
45
+
46
+ node_for(existing[1])
47
+ end
48
+
49
+ # Build a {Graph::Node} with outputs etc.
50
+ def node_for(node_attributes)
51
+ Node(
52
+ node_attributes.task,
53
+ node_attributes.id,
54
+ node_attributes.outputs, # [#<struct Trailblazer::Activity::Output signal=Trailblazer::Activity::Right, semantic=:success>]
55
+ outgoings_for(node_attributes),
56
+ node_attributes.data,
57
+ )
58
+ end
59
+
60
+ def Node(*args)
61
+ Node.new(*args).freeze
62
+ end
63
+
64
+ Node = Struct.new(:task, :id, :outputs, :outgoings, :data)
65
+ Outgoing = Struct.new(:output, :task)
66
+
67
+ def outgoings_for(node)
68
+ outputs = node.outputs
69
+ connections = @map[node.task]
70
+
71
+ connections.collect do |signal, target|
72
+ output = outputs.find { |out| out.signal == signal }
73
+ Outgoing.new(output, target)
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.Graph(*args)
79
+ Graph.new(*args)
80
+ end
81
+ end # Graph
82
+ end
83
+ end
@@ -2,61 +2,13 @@ module Trailblazer
2
2
  module Developer
3
3
  module_function
4
4
 
5
- def render(activity, **options)
6
- Render::Circuit.(activity, **options)
7
- end
8
-
9
- module Render
10
- module Circuit
11
- module_function
12
-
13
- # Render an {Activity}'s circuit as a simple hash.
14
- def call(activity, path: nil, **options)
15
- if path # TODO: move to place where all renderers can use this logic!
16
- node, _, graph = Developer::Introspect.find_path(activity, path)
17
- activity = node.task
18
- end
19
-
20
- graph = Activity::Introspect::Graph(activity)
21
-
22
- circuit_hash(graph, **options)
23
- end
24
-
25
- def circuit_hash(graph, **options)
26
- content = graph.collect do |node|
27
- conns = node.outgoings.collect do |outgoing|
28
- " {#{outgoing.output.signal}} => #{inspect_with_matcher(outgoing.task, **options)}"
29
- end
30
-
31
- [ inspect_with_matcher(node.task, **options), conns.join("\n") ]
32
- end
33
-
34
- content = content.join("\n")
35
-
36
- "\n#{content}".gsub(/0x\w+/, "0x")
37
- end
38
-
39
- # If Ruby had pattern matching, this function wasn't necessary.
40
- def inspect_with_matcher(task, inspect_task: method(:inspect_task), inspect_end: method(:inspect_end))
41
- return inspect_task.(task) unless task.kind_of?(Trailblazer::Activity::End)
42
- inspect_end.(task)
43
- end
44
-
45
- def inspect_task(task)
46
- task.inspect
47
- end
48
-
49
- def inspect_end(task)
50
- class_name = Render::Circuit.strip(task.class)
51
- options = task.to_h
52
-
53
- "#<#{class_name}/#{options[:semantic].inspect}>"
54
- end
55
-
56
- def self.strip(string)
57
- string.to_s.sub("Trailblazer::Activity::", "")
58
- end
5
+ def render(activity, path: nil, **options)
6
+ if path # TODO: move to place where all renderers can use this logic!
7
+ node, _, graph = Developer::Introspect.find_path(activity, path)
8
+ activity = node.task
59
9
  end
10
+
11
+ Activity::Introspect::Render.(activity, **options)
60
12
  end
61
13
  end
62
14
  end
@@ -15,7 +15,7 @@ module Trailblazer
15
15
  module_function
16
16
 
17
17
  def call(operation, style: :line)
18
- graph = Activity::Introspect::Graph(operation)
18
+ graph = Introspect::Graph(operation)
19
19
 
20
20
  rows = graph.collect do |node, i|
21
21
  next if node[:data][:stop_event] # DISCUSS: show this?
@@ -18,7 +18,7 @@ module Trailblazer
18
18
 
19
19
  # @param activity Activity
20
20
  def self.task_wrap_for_activity(activity, **)
21
- activity[:wrap_static]
21
+ activity.to_h[:config][:wrap_static]
22
22
  end
23
23
 
24
24
  def self.render_pipeline(pipeline, level)
@@ -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