trailblazer-developer 0.0.27 → 0.1.0

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