flows 0.4.0 → 0.5.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/.mdlrc +1 -1
- data/.reek.yml +12 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +1 -1
- data/README.md +12 -2
- data/Rakefile +1 -1
- data/bin/all_the_errors +8 -0
- data/bin/errors +12 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/docs/README.md +1 -1
- data/lib/flows/contract/case_eq.rb +3 -1
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +1 -0
- data/lib/flows/flow/router/custom.rb +5 -0
- data/lib/flows/flow/router/simple.rb +5 -0
- data/lib/flows/flow/router.rb +4 -0
- data/lib/flows/flow.rb +21 -0
- data/lib/flows/plugin/dependency_injector.rb +5 -5
- data/lib/flows/plugin/output_contract/dsl.rb +15 -3
- data/lib/flows/plugin/output_contract/wrapper.rb +14 -12
- data/lib/flows/plugin/output_contract.rb +1 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin.rb +1 -0
- data/lib/flows/railway/dsl.rb +3 -2
- data/lib/flows/result/do.rb +6 -8
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +5 -56
- data/lib/flows/shared_context_pipeline/mutation_step.rb +6 -8
- data/lib/flows/shared_context_pipeline/step.rb +6 -8
- data/lib/flows/shared_context_pipeline/track.rb +2 -15
- data/lib/flows/shared_context_pipeline/track_list.rb +11 -6
- data/lib/flows/shared_context_pipeline/wrap.rb +64 -0
- data/lib/flows/shared_context_pipeline.rb +109 -26
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +40 -51
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +39 -52
- data/lib/flows/util/inheritable_singleton_vars.rb +22 -15
- data/lib/flows/util/prepend_to_class.rb +43 -9
- data/lib/flows/version.rb +1 -1
- metadata +18 -2
@@ -22,6 +22,7 @@ module Flows
|
|
22
22
|
#
|
23
23
|
# * `success_with(status, &block)` - defines contract for a successful result with status `status`.
|
24
24
|
# * `failure_with(status, &block)` - defines contract for a failure result with status `status`.
|
25
|
+
# * `skip_output_contract` - disables contract check and transformation for current class and children.
|
25
26
|
#
|
26
27
|
# @example with one possible output contract
|
27
28
|
# class DoJob
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module Profiler
|
4
|
+
# @api private
|
5
|
+
module Injector
|
6
|
+
class << self
|
7
|
+
def make_module(method_name)
|
8
|
+
Module.new.tap do |mod|
|
9
|
+
add_included(mod, method_name)
|
10
|
+
add_extended(mod, method_name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def add_included(mod, method_name)
|
17
|
+
mod.define_method(:included) do |target|
|
18
|
+
raise 'must be included into class' unless target.is_a?(Class)
|
19
|
+
|
20
|
+
target.prepend Wrapper.make_instance_wrapper(method_name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_extended(mod, method_name)
|
25
|
+
mod.define_method(:extended) do |target|
|
26
|
+
raise 'must be extended into class' unless target.is_a?(Class)
|
27
|
+
|
28
|
+
target.singleton_class.prepend(Wrapper.make_singleton_wrapper(method_name))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module Profiler
|
4
|
+
class Report
|
5
|
+
# @api private
|
6
|
+
Event = Struct.new(:method_class, :method_type, :method_name, :data) do
|
7
|
+
def subject
|
8
|
+
"#{method_class}#{delimeter}#{method_name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def delimeter
|
14
|
+
case method_type
|
15
|
+
when :instance then '#'
|
16
|
+
when :singleton then '.'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @api private
|
22
|
+
#
|
23
|
+
# Method execution start event.
|
24
|
+
class StartEvent < Event
|
25
|
+
def to_s
|
26
|
+
"start: #{subject}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
#
|
32
|
+
# Method execution finish event.
|
33
|
+
#
|
34
|
+
# Data is an execution time in microseconds.
|
35
|
+
class FinishEvent < Event
|
36
|
+
def to_s
|
37
|
+
"finish(#{data} microseconds): #{subject}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module Profiler
|
4
|
+
class Report
|
5
|
+
class Flat < Tree
|
6
|
+
# @api private
|
7
|
+
class MethodReport
|
8
|
+
attr_reader :root_node
|
9
|
+
attr_reader :calculated_nodes
|
10
|
+
|
11
|
+
def initialize(root_node, *calculated_nodes)
|
12
|
+
@root_node = root_node
|
13
|
+
@calculated_nodes = calculated_nodes
|
14
|
+
|
15
|
+
raise 'no single node provided' if calculated_nodes.empty?
|
16
|
+
raise 'calculated_nodes must be about the same subject' unless nodes_have_same_subject
|
17
|
+
end
|
18
|
+
|
19
|
+
def subject
|
20
|
+
@subject ||= calculated_nodes.first.subject
|
21
|
+
end
|
22
|
+
|
23
|
+
def count
|
24
|
+
@count ||= calculated_nodes.map(&:count).sum
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_self_ms
|
28
|
+
@total_self_ms ||= calculated_nodes.map(&:total_self_ms).sort.sum
|
29
|
+
end
|
30
|
+
|
31
|
+
def total_self_percentage
|
32
|
+
@total_self_percentage ||= calculated_nodes
|
33
|
+
.map { |node| node.total_self_percentage(root_node) }
|
34
|
+
.sort
|
35
|
+
.sum
|
36
|
+
end
|
37
|
+
|
38
|
+
def avg_self_ms
|
39
|
+
@avg_self_ms ||= total_self_ms / count
|
40
|
+
end
|
41
|
+
|
42
|
+
def direct_subcalls
|
43
|
+
@direct_subcalls ||= calculated_nodes
|
44
|
+
.flat_map { |node| node.children.map(&:subject) }
|
45
|
+
.uniq
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_h
|
49
|
+
@to_h ||= {
|
50
|
+
subject: subject,
|
51
|
+
count: count,
|
52
|
+
total_self_ms: total_self_ms,
|
53
|
+
total_self_percentage: total_self_percentage,
|
54
|
+
avg_self_ms: avg_self_ms,
|
55
|
+
direct_subcalls: direct_subcalls
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
[
|
61
|
+
'',
|
62
|
+
"- #{subject} -",
|
63
|
+
"called: #{count} time(s)",
|
64
|
+
"total self execution time: #{total_self_ms.truncate(2)}ms",
|
65
|
+
"total self percentage: #{total_self_percentage.truncate(2)}%",
|
66
|
+
"average self execution time: #{avg_self_ms.truncate(2)}ms",
|
67
|
+
"direct subcalls: #{direct_subcalls.join(', ')}"
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def nodes_have_same_subject
|
74
|
+
calculated_nodes.all? { |node| node.subject == subject }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'flat/method_report'
|
2
|
+
|
3
|
+
module Flows
|
4
|
+
module Plugin
|
5
|
+
module Profiler
|
6
|
+
class Report
|
7
|
+
# Flat report. Merges similar calls, hides execution structure.
|
8
|
+
#
|
9
|
+
# It's a variation of a {Rport::Tree} where all calls of the same method
|
10
|
+
# are combined into a one first-level entry.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# Flows::Plugin::Profiler.profile(:flat) do
|
14
|
+
# # some code here
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# puts Flows::Plugin::Profiler.last_report
|
18
|
+
class Flat < Tree
|
19
|
+
def to_a
|
20
|
+
method_reports.map(&:to_h)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
method_reports.map(&:to_s).join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def method_reports
|
30
|
+
@method_reports ||= root_calculated_node
|
31
|
+
.group_by_subject
|
32
|
+
.values
|
33
|
+
.map { |nodes| MethodReport.new(root_calculated_node, *nodes) }
|
34
|
+
.sort_by(&:total_self_ms)
|
35
|
+
.reverse
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module Profiler
|
4
|
+
class Report
|
5
|
+
class Tree < Report
|
6
|
+
# @api private
|
7
|
+
class CalculatedNode
|
8
|
+
MICROSECONDS_IN_MILLISECOND = 1000.0
|
9
|
+
|
10
|
+
attr_reader :children
|
11
|
+
|
12
|
+
def initialize(node)
|
13
|
+
@node = node
|
14
|
+
@children = node.children
|
15
|
+
.map { |child| self.class.new(child) }
|
16
|
+
.sort_by(&:total_ms)
|
17
|
+
.reverse
|
18
|
+
end
|
19
|
+
|
20
|
+
def subject
|
21
|
+
@node.subject
|
22
|
+
end
|
23
|
+
|
24
|
+
def count
|
25
|
+
@count ||= @node.executions.count
|
26
|
+
end
|
27
|
+
|
28
|
+
def total_ms
|
29
|
+
@total_ms ||= @node.executions.sort.sum / MICROSECONDS_IN_MILLISECOND
|
30
|
+
end
|
31
|
+
|
32
|
+
def avg_ms
|
33
|
+
@avg_ms ||= total_ms / count
|
34
|
+
end
|
35
|
+
|
36
|
+
def children_ms
|
37
|
+
@children_ms ||= children.map(&:total_ms).sort.sum
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_self_ms
|
41
|
+
@total_self_ms ||= total_ms - children_ms
|
42
|
+
end
|
43
|
+
|
44
|
+
def total_self_percentage(root_node = self)
|
45
|
+
@total_self_percentage ||= total_self_ms / root_node.children_ms * 100.0
|
46
|
+
end
|
47
|
+
|
48
|
+
def total_percentage(root_node = self)
|
49
|
+
@total_percentage ||= total_ms / root_node.children_ms * 100.0
|
50
|
+
end
|
51
|
+
|
52
|
+
def avg_self_ms
|
53
|
+
@avg_self_ms ||= total_self_ms / count
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_h(root_node = self) # rubocop:disable Metrics/MethodLength
|
57
|
+
@to_h ||= {
|
58
|
+
subject: subject,
|
59
|
+
count: count,
|
60
|
+
total_ms: total_ms,
|
61
|
+
total_percentage: total_percentage(root_node),
|
62
|
+
total_self_ms: total_self_ms,
|
63
|
+
total_self_percentage: total_self_percentage(root_node),
|
64
|
+
avg_ms: avg_ms,
|
65
|
+
avg_self_ms: avg_self_ms,
|
66
|
+
nested: children.map { |node| node.to_h(root_node) }
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s(root_node = self)
|
71
|
+
@to_s ||= (base_text_list(root_node) + childeren_text_list(root_node)).join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
# :reek:DuplicateMethodCall
|
75
|
+
# :reek:NestedIterators
|
76
|
+
def group_by_subject
|
77
|
+
@group_by_subject ||= (
|
78
|
+
[children.group_by(&:subject)] + children.map(&:group_by_subject)
|
79
|
+
).each_with_object({}) do |group, result|
|
80
|
+
group.each do |subject, nodes|
|
81
|
+
result[subject] ||= []
|
82
|
+
result[subject] += nodes
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def base_text_list(root_node) # rubocop:disable Metrics/MethodLength
|
90
|
+
[
|
91
|
+
'',
|
92
|
+
"- #{subject} -",
|
93
|
+
"called: #{count} time(s)",
|
94
|
+
"total execution time: #{total_ms.truncate(2)}ms",
|
95
|
+
"total percentage: #{total_percentage(root_node).truncate(2)}%",
|
96
|
+
"total self execution time: #{total_self_ms.truncate(2)}ms",
|
97
|
+
"total self percentage: #{total_self_percentage(root_node).truncate(2)}%",
|
98
|
+
"average execution time: #{avg_ms.truncate(2)}ms",
|
99
|
+
"average self execution time: #{avg_self_ms.truncate(2)}ms"
|
100
|
+
]
|
101
|
+
end
|
102
|
+
|
103
|
+
def childeren_text_list(root_node)
|
104
|
+
return [] if @children.empty?
|
105
|
+
|
106
|
+
children.map { |node| node.to_s(root_node) }
|
107
|
+
.join("\n")
|
108
|
+
.split("\n")
|
109
|
+
.map { |str| '| ' + str }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module Profiler
|
4
|
+
class Report
|
5
|
+
class Tree < Report
|
6
|
+
# @api private
|
7
|
+
class Node
|
8
|
+
attr_reader :subject
|
9
|
+
attr_reader :executions
|
10
|
+
|
11
|
+
def initialize(subject:)
|
12
|
+
@subject = subject
|
13
|
+
@children = {}
|
14
|
+
@cache = {}
|
15
|
+
|
16
|
+
@executions = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](subject)
|
20
|
+
@children[subject] ||= Node.new(subject: subject)
|
21
|
+
end
|
22
|
+
|
23
|
+
def children
|
24
|
+
@children.values
|
25
|
+
end
|
26
|
+
|
27
|
+
def register_execution(microseconds)
|
28
|
+
@executions << microseconds
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative 'tree/node'
|
2
|
+
require_relative 'tree/calculated_node'
|
3
|
+
|
4
|
+
module Flows
|
5
|
+
module Plugin
|
6
|
+
module Profiler
|
7
|
+
class Report
|
8
|
+
# Tree report. Merges similar calls, saves execution structure (who called whom).
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# Flows::Plugin::Profiler.profile(:tree) do
|
12
|
+
# # some code here
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# puts Flows::Plugin::Profiler.last_report
|
16
|
+
class Tree < Report
|
17
|
+
# Returns tree report as Ruby data structs.
|
18
|
+
#
|
19
|
+
# @return [Array<Hash>] tree report.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# [
|
23
|
+
# {
|
24
|
+
# subject: 'MyClass#call',
|
25
|
+
# count: 2,
|
26
|
+
# total_ms: 100.0,
|
27
|
+
# total_self_ms: 80.0,
|
28
|
+
# total_self_percentage: 80.0,
|
29
|
+
# avg_ms: 50.0,
|
30
|
+
# avg_self_ms: 40.0,
|
31
|
+
# nested: [
|
32
|
+
# {
|
33
|
+
# subject: 'MyClass#another_method',
|
34
|
+
# count: 1,
|
35
|
+
# total_ms: 20.0,
|
36
|
+
# total_self_ms: 20.0,
|
37
|
+
# total_self_percentage: 20.0,
|
38
|
+
# avg_ms: 20.0,
|
39
|
+
# avg_self_ms: 20.0,
|
40
|
+
# nested: []
|
41
|
+
# }
|
42
|
+
# ]
|
43
|
+
# }
|
44
|
+
# ]
|
45
|
+
def to_a
|
46
|
+
root_calculated_node.children.map { |node| node.to_h(root_calculated_node) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def add(*)
|
50
|
+
forget_memoized_values
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
root_calculated_node.children.map { |node| node.to_s(root_calculated_node) }.join
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def forget_memoized_values
|
61
|
+
@root_node = nil
|
62
|
+
@root_calculated_node = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def root_calculated_node
|
66
|
+
@root_calculated_node ||= CalculatedNode.new(root_node)
|
67
|
+
end
|
68
|
+
|
69
|
+
def root_node
|
70
|
+
@root_node ||= Node.new(subject: :ROOT).tap do |root_node|
|
71
|
+
events.each_with_object([root_node]) do |event, node_path|
|
72
|
+
current_node = node_path.last
|
73
|
+
|
74
|
+
case event
|
75
|
+
when StartEvent then process_start_event(node_path, current_node, event)
|
76
|
+
when FinishEvent then process_finish_event(node_path, current_node, event)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# :reek:UtilityFunction
|
83
|
+
def process_start_event(node_path, current_node, event)
|
84
|
+
node_path << current_node[event.subject]
|
85
|
+
end
|
86
|
+
|
87
|
+
# :reek:UtilityFunction :reek:FeatureEnvy
|
88
|
+
def process_finish_event(node_path, current_node, event)
|
89
|
+
raise 'Invalid profiling events detected' if event.subject != current_node.subject
|
90
|
+
|
91
|
+
current_node.register_execution(event.data)
|
92
|
+
node_path.pop
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|