test-prof-autopilot 0.0.7 → 0.1.0.pre.2

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: fa21fa398d9ae8f3a79906b3f33d0d75148e9c2ee7ebb5d9900f2302eaefeac1
4
- data.tar.gz: cc151f75133d9d71d370c31ba0e18c0466269bfa3241b2adb08005073a981699
3
+ metadata.gz: 8ee9039d6d83bbf54010e2ebda79747d5ad7511cb7cff65eb1d1064f72561692
4
+ data.tar.gz: fc3b28fc007dcfefddf66dd4b045befb745db2aa3dc09ca591ae3a75cee45b73
5
5
  SHA512:
6
- metadata.gz: c4d70b1e42e3aabf5e1c7dc6f9c4403b863e5fc9b95af70cb291d48b1fc4760c91e697b96ff903099a364b3a667e53f151f7934d929e41718a153f88b0a5acc4
7
- data.tar.gz: b51e1abe16366626f57c347566d026e504313df6e3b4813b72b95eeaf8ccff9513f7e38729d3ab13fcc1b2afb6756152feb6c5a9b784fbc7433f607b2e7e1585
6
+ metadata.gz: 9540aa726621514223f0f48a02b27e13c1082b27596093d610b19fd0d885dcb416f6336c03e631fb4d51a10ea57a21ccbb671312b0ca9a02a36a8e73c5225412
7
+ data.tar.gz: 6d16c822426a62e6ac7ad8d8a0d747d0e61e2850e23252ec9b9753f5ec735cb9b64ff28bb5e071614359a04314ba8eff25d08eb433e4319bfbd53337562808ce
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Vladimir Dementyev, Ruslan Shakirov
3
+ Copyright (c) 2021-2023 Vladimir Dementyev, Ruslan Shakirov
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # TestProf Autopilot (PoC)
1
+ [![Gem Version](https://badge.fury.io/rb/test-prof-autopilot.svg)](https://rubygems.org/gems/test-prof-autopilot)
2
+ [![Build](https://github.com/test-prof/test-prof-autopilot/workflows/Build/badge.svg)](https://github.com/test-prof/test-prof-autopilot/actions)
3
+
4
+ # TestProf Autopilot
2
5
 
3
6
  [TestProf][] has been used by many Ruby/Rails teams to optimize their test suites performance for a while.
4
7
 
@@ -6,23 +9,11 @@ Usually, it takes a decent amount of time to profile the test suite initially: w
6
9
 
7
10
  There are some common patterns in the way we use TestProf, for example: we run StackProf/RubyProf multiple times for different test samples, or we run EventProf for `factory.create` and then use FactoryProf for the slowest tests.
8
11
 
9
- It seems that there is a room for optimization here: we can automate this common tasks, make robots do all the repetition.
10
-
11
- This project (codename _TestProf Autopilot_) aims to solve this problem.
12
-
13
- ## Usage (proposal)
14
-
15
- Use a CLI to run a specific tests profiling plan:
16
-
17
- ```sh
18
- auto-test-prof -i plan.rb -с "bundle exec rspec"
19
- ```
20
-
21
- We specify the base command to run tests via the `-c` option.
12
+ It seems that there is a room for optimization here: we can automate this common tasks, make robots do all the repetition. And here comes **TestProf Autopilot**!
22
13
 
23
- Profiling plan is a Ruby file using a custom DSL to run profilers and access their reports:
14
+ ## Usage
24
15
 
25
- Here is an example #2:
16
+ First, write a test profiling plan in a Ruby file. For example, here is how you can perform StackProf profiling multiple times against different random subsets and aggregate the results:
26
17
 
27
18
  ```ruby
28
19
  # This plan runs multiple test samples and collects StackProf data.
@@ -36,41 +27,68 @@ aggregate(3) { run :stackprof, sample: 100 }
36
27
 
37
28
  # `report` returns the latest generated report (both `run` and `aggregate` set this value automatically)
38
29
  # `#methods` returns the list of collected reports sorted by their popularity
39
- # `info` prints the information (ideally, it should be human-readable)
40
- info report.methods.take(5)
30
+ puts report.methods.take(5)
41
31
  ```
42
32
 
43
- And example #2:
33
+ Now, you can use the `auto-test-prof` command to execute the plan:
44
34
 
45
- ```ruby
46
- # This plan first launch the test suite and collect the information about the time spent in factories.
47
- # Then it runs FactoryProf for the slowest tests and display the information.
35
+ ```sh
36
+ auto-test-prof -i plan.rb "bundle exec rspec"
37
+ ```
38
+
39
+ We specify the base command to run tests via the `-c` option. If you omit the command option, Autopilot would fall back to either `bundle exec rspec` or `bundle exec rake test` depending on the presense of the `spec/` and `test/` directories, respectively.
48
40
 
49
- run :event_prof, event: "factory.create"
50
- run :factory_prof, paths: report.paths
41
+ ### Merging results
51
42
 
52
- info report
43
+ Autopilot also allows you to merge reports created with it (using the `#save` method). That's useful when you profile tests on CI and want to see the aggregated results. For example, when using TagProf:
44
+
45
+ ```ruby
46
+ run :tag_prof, events: ["factory.create"]
47
+
48
+ save report, file_name: "tag_prof_#{ENV["CI_NODE_INDEX"]}"
53
49
  ```
54
50
 
55
- ### Notes on implementation
51
+ Then, assuming all reports were downloaded:
56
52
 
57
- We use `#run` method to launch tests with profiling. Each profiler has a uniq name (which is the first argument) and some options.
58
- Some options are common for all profilers (e.g., `sample:` and `paths:`).
53
+ ```sh
54
+ $ auto-test-prof --merge tag_prof --reports tag_prof_*.json
59
55
 
60
- ## Installation
56
+ Merging tag_prof reports at tag_prof_1.json, tag_prof_2.json, tag_prof_3.json
61
57
 
62
- Adding to a gem:
58
+ [TEST PROF] TagProf report for type
63
59
 
64
- ```ruby
65
- # my-cool-gem.gemspec
66
- Gem::Specification.new do |spec|
67
- # ...
68
- spec.add_dependency "test-prof-autopilot"
69
- # ...
70
- end
60
+ type time factory.create total %total %time avg
61
+
62
+ model 28:08.654 19:58.371 1730 56.44 46.23 00:00.976
63
+ service 20:56.071 16:14.435 808 29.18 28.35 00:01.554
64
+ api 04:48.179 03:54.178 214 7.32 4.78 00:01.346
65
+ ...
71
66
  ```
72
67
 
73
- Or adding to your project:
68
+ ### API
69
+
70
+ - `run(profiler_name, **options)`: launch the test command with the specified profiler activated; options depend on the profiler, but there are some commont: `sample: <number>` — enables sampling, `paths: <array of paths>` — adds the list of paths to the command.
71
+
72
+ - `info(report)`: shows the report in the console
73
+
74
+ - `save(report, path)`: store
75
+
76
+ - `aggregate(num_calls) { ... }`: aggregates reports obtained by calling the block `num_calls` times.
77
+
78
+ You can find more examples in the [examples/](examples/) folder.
79
+
80
+ ### Supported profilers
81
+
82
+ Currently, Autopilot supports the following Test Prof profilers:
83
+
84
+ - [EventProf](https://test-prof.evilmartians.io/profilers/event_prof) (as `:event_prof`)
85
+ - [TagProf](https://test-prof.evilmartians.io/profilers/tag_prof) (as `:tag_prof`)
86
+ - [StackProf](https://test-prof.evilmartians.io/profilers/stack_prof) (as `:stack_prof`)
87
+ - [FactoryProf](https://test-prof.evilmartians.io/profilers/factory_prof) (as `:factory_prof`)
88
+
89
+ ## Installation
90
+
91
+ Add the gem to your project:
74
92
 
75
93
  ```ruby
76
94
  # Gemfile
@@ -79,4 +97,8 @@ group :development, :test do
79
97
  end
80
98
  ```
81
99
 
100
+ Make sure `test-prof-autopilot` is required in your test environment.
101
+
102
+ That's it!
103
+
82
104
  [TestProf]: https://test-prof.evilmartians.io/
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/autopilot/version"
4
+ require "test_prof/autopilot"
@@ -2,11 +2,14 @@
2
2
 
3
3
  require "optparse"
4
4
  require "test_prof/autopilot/runner"
5
+ require "test_prof/autopilot/logging"
5
6
  require "test_prof/autopilot/merger"
6
7
 
7
8
  module TestProf
8
9
  module Autopilot
9
10
  class CLI
11
+ include Logging
12
+
10
13
  attr_reader :command, :plan_path, :mode, :merge_type, :report_paths
11
14
 
12
15
  def run(args = ARGV)
@@ -15,6 +18,8 @@ module TestProf
15
18
  optparser.parse!(args)
16
19
 
17
20
  if mode == "runner"
21
+ infer_command! unless command
22
+
18
23
  raise "Test command must be specified. See -h for options" unless command
19
24
 
20
25
  raise "Plan path must be specified. See -h for options" unless plan_path
@@ -67,6 +72,18 @@ module TestProf
67
72
  end
68
73
  end
69
74
  end
75
+
76
+ def infer_command!
77
+ if Dir.exist?("spec")
78
+ @command = "bundle exec rspec"
79
+ elsif Dir.exist?("test")
80
+ @command = "bundle exec rake test"
81
+ end
82
+
83
+ if @command
84
+ Logging.log "No command specified, using: #{@command}"
85
+ end
86
+ end
70
87
  end
71
88
  end
72
89
  end
@@ -7,6 +7,8 @@ module TestProf
7
7
  # Module is used for commands execution in child process.
8
8
  module CommandExecutor
9
9
  def execute(env, command)
10
+ env.merge!("TEST_PROF_AUTOPILOT_ENABLED" => "true")
11
+
10
12
  Open3.popen2e(env, command) do |_stdin, stdout_and_stderr, _wait_thr|
11
13
  while (line = stdout_and_stderr.gets)
12
14
  Logging.log line
@@ -24,8 +24,8 @@ module TestProf
24
24
 
25
25
  def initialize
26
26
  @output = $stdout
27
- @tmp_dir = "tmp/test_prof_autopilot"
28
- @artifacts_dir = "test_prof_autopilot"
27
+ @tmp_dir = ENV.fetch("TEST_PROF_AUTOPILOT_TMP_DIR", "tmp/test_prof_autopilot")
28
+ @artifacts_dir = ENV.fetch("TEST_PROF_AUTOPILOT_DIR", "test_prof_autopilot")
29
29
  @merge_format = "info"
30
30
  end
31
31
  end
@@ -25,6 +25,10 @@ module TestProf
25
25
  def build_env
26
26
  super.tap do |env|
27
27
  env["EVENT_PROF"] = @options[:event]
28
+ env["EVENT_PROF_TOP"] = @options[:top_count].to_s if @options[:top_count]
29
+ env["EVENT_PROF_EXAMPLES"] = "1" if @options[:per_example]
30
+ env["EVENT_PROF_RANK"] = @options[:rank_by].to_s if @options[:rank_by]
31
+ env["EVENT_PROF_STAMP"] = @options[:stamp] if @options[:stamp]
28
32
  end
29
33
  end
30
34
  end
@@ -1,17 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- using(Module.new do
5
- refine FactoryProf::Result do
6
- def to_json
7
- {
8
- stacks: stacks,
9
- raw_stats: raw_stats
10
- }.to_json
11
- end
12
- end
13
- end)
14
-
15
4
  module Autopilot
16
5
  module FactoryProf
17
6
  # Class is used for writing :factory_prof report in different formats
@@ -13,10 +13,10 @@ module TestProf
13
13
 
14
14
  class << self
15
15
  def invoke(type, paths)
16
- Logging.log "Merging #{type} reports at #{paths.join(", ")}..."
17
-
18
16
  paths = paths.flat_map(&Dir.method(:glob))
19
17
 
18
+ Logging.log "Merging #{type} reports at #{paths.join(", ")}..."
19
+
20
20
  new(type, paths).print_report
21
21
  end
22
22
  end
@@ -24,32 +24,53 @@ module TestProf
24
24
  end
25
25
 
26
26
  def merge(other)
27
- f1, f2 = data[:frames], other.data[:frames]
28
-
29
- frames = (f1.keys + f2.keys).uniq.each_with_object({}) do |id, hash|
30
- if f1[id].nil?
31
- hash[id] = f2[id]
32
- elsif f2[id]
33
- hash[id] = f1[id]
34
- hash[id][:total_samples] += f2[id][:total_samples]
35
- hash[id][:samples] += f2[id][:samples]
36
- if f2[id][:edges]
37
- edges = hash[id][:edges] ||= {}
38
- f2[id][:edges].each do |edge, weight|
39
- edges[edge] ||= 0
40
- edges[edge] += weight
41
- end
27
+ ids_mapping = generate_ids_mapping(data[:frames], other.data[:frames])
28
+
29
+ frames = data[:frames].dup
30
+
31
+ other.data[:frames].each do |id, new_frame|
32
+ frame =
33
+ if ids_mapping[id]
34
+ frames[ids_mapping[id]]
35
+ else
36
+ frames[id] = empty_frame_from(new_frame)
42
37
  end
43
- if f2[id][:lines]
44
- lines = hash[id][:lines] ||= {}
45
- f2[id][:lines].each do |line, weight|
46
- lines[line] = add_lines(lines[line], weight)
38
+
39
+ frame[:total_samples] += new_frame[:total_samples]
40
+ frame[:samples] += new_frame[:samples]
41
+
42
+ if new_frame[:edges]
43
+ edges = (frame[:edges] ||= {})
44
+
45
+ new_frame[:edges].each do |edge, weight|
46
+ old_edge = ids_mapping[edge]
47
+
48
+ if edges[old_edge]
49
+ edges[old_edge] += weight
50
+ else
51
+ edges[old_edge] = weight
47
52
  end
48
53
  end
49
- else
50
- hash[id] = f1[id]
51
54
  end
52
- hash
55
+
56
+ if new_frame[:lines]
57
+ lines = (frame[:lines] ||= {})
58
+
59
+ new_frame[:lines].each do |line, weight|
60
+ old_line = ids_mapping[line]
61
+
62
+ lines[old_line] =
63
+ if lines[old_line]
64
+ add_lines(lines[old_line], weight)
65
+ else
66
+ weight
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ converted_raw = other.data[:raw].map do |raw|
73
+ ids_mapping[raw] || raw
53
74
  end
54
75
 
55
76
  d1, d2 = data, other.data
@@ -62,12 +83,38 @@ module TestProf
62
83
  missed_samples: d1[:missed_samples] + d2[:missed_samples],
63
84
  metadata: d1[:metadata].merge(d2[:metadata]),
64
85
  frames: frames,
65
- raw: d1[:raw] + d2[:raw],
86
+ raw: d1[:raw] + converted_raw,
66
87
  raw_timestamp_deltas: d1[:raw_timestamp_deltas] + d2[:raw_timestamp_deltas]
67
88
  }
68
89
 
69
90
  self.class.new(data)
70
91
  end
92
+
93
+ def generate_ids_mapping(frames, other_frames)
94
+ old_fingerprints = frames_to_fingerprints(frames)
95
+ new_fingerprints = frames_to_fingerprints(other_frames)
96
+
97
+ new_fingerprints.each_with_object({}) do |(fingerprint, frame), hash|
98
+ next hash unless old_fingerprints[fingerprint]
99
+
100
+ hash[frame[:id]] = old_fingerprints[fingerprint][:id]
101
+ end
102
+ end
103
+
104
+ def frames_to_fingerprints(frames)
105
+ frames.each_with_object({}) do |(id, frame), hash|
106
+ fingerprint = [frame[:name], frame[:file], frame[:line]].compact.map(&:to_s).join("/")
107
+ hash[fingerprint] = frame.merge(id: id)
108
+ hash
109
+ end
110
+ end
111
+
112
+ def empty_frame_from(frame)
113
+ frame.slice(:name, :file, :line).merge(
114
+ total_samples: 0,
115
+ samples: 0
116
+ )
117
+ end
71
118
  end
72
119
  end
73
120
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TestProf
4
4
  module Autopilot # :nodoc:
5
- VERSION = "0.0.7"
5
+ VERSION = "0.1.0.pre.2"
6
6
  end
7
7
  end
@@ -2,7 +2,12 @@
2
2
 
3
3
  require "test-prof"
4
4
  require "test_prof/autopilot/configuration"
5
- require "test_prof/autopilot/patches/event_prof_patch"
6
- require "test_prof/autopilot/patches/tag_prof_patch"
7
- require "test_prof/autopilot/patches/factory_prof_patch"
8
- require "test_prof/autopilot/patches/stack_prof_patch"
5
+
6
+ # We only load the patches when tests are executed by autopilot
7
+ # TODO: We should move the patches into TestProf itself as `--format=json`.
8
+ if ENV["TEST_PROF_AUTOPILOT_ENABLED"] == "true"
9
+ require "test_prof/autopilot/patches/event_prof_patch"
10
+ require "test_prof/autopilot/patches/tag_prof_patch"
11
+ require "test_prof/autopilot/patches/factory_prof_patch"
12
+ require "test_prof/autopilot/patches/stack_prof_patch"
13
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-prof-autopilot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.1.0.pre.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Shakirov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-06-14 00:00:00.000000000 Z
12
+ date: 2023-11-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-prof
@@ -131,7 +131,13 @@ files:
131
131
  homepage: http://github.com/test-prof/test-prof-autopilot
132
132
  licenses:
133
133
  - MIT
134
- metadata: {}
134
+ metadata:
135
+ bug_tracker_uri: https://github.com/test-prof/test-prof-autopilot/issues
136
+ changelog_uri: https://github.com/test-prof/test-prof-autopilot/blob/master/CHANGELOG.md
137
+ documentation_uri: https://test-prof.evilmartians.io/
138
+ homepage_uri: https://test-prof.evilmartians.io/
139
+ source_code_uri: https://github.com/test-prof/test-prof-autopilot
140
+ funding_uri: https://github.com/sponsors/test-prof
135
141
  post_install_message:
136
142
  rdoc_options: []
137
143
  require_paths:
@@ -143,11 +149,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
143
149
  version: '2.7'
144
150
  required_rubygems_version: !ruby/object:Gem::Requirement
145
151
  requirements:
146
- - - ">="
152
+ - - ">"
147
153
  - !ruby/object:Gem::Version
148
- version: '0'
154
+ version: 1.3.1
149
155
  requirements: []
150
- rubygems_version: 3.3.7
156
+ rubygems_version: 3.4.20
151
157
  signing_key:
152
158
  specification_version: 4
153
159
  summary: Automatic TestProf runner