test-prof-autopilot 0.0.2
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +82 -0
- data/bin/auto-test-prof +16 -0
- data/lib/test-prof-autopilot.rb +3 -0
- data/lib/test_prof/autopilot/cli.rb +45 -0
- data/lib/test_prof/autopilot/command_executor.rb +20 -0
- data/lib/test_prof/autopilot/configuration.rb +30 -0
- data/lib/test_prof/autopilot/dsl.rb +59 -0
- data/lib/test_prof/autopilot/event_prof/printer.rb +44 -0
- data/lib/test_prof/autopilot/event_prof/profiling_executor.rb +32 -0
- data/lib/test_prof/autopilot/event_prof/report.rb +31 -0
- data/lib/test_prof/autopilot/factory_prof/printer.rb +44 -0
- data/lib/test_prof/autopilot/factory_prof/profiling_executor.rb +27 -0
- data/lib/test_prof/autopilot/factory_prof/report.rb +27 -0
- data/lib/test_prof/autopilot/logging.rb +13 -0
- data/lib/test_prof/autopilot/patches/event_prof_patch.rb +53 -0
- data/lib/test_prof/autopilot/patches/factory_prof_patch.rb +44 -0
- data/lib/test_prof/autopilot/patches/stack_prof_patch.rb +26 -0
- data/lib/test_prof/autopilot/profiling_executor/base.rb +61 -0
- data/lib/test_prof/autopilot/registry.rb +20 -0
- data/lib/test_prof/autopilot/report_builder.rb +26 -0
- data/lib/test_prof/autopilot/runner.rb +30 -0
- data/lib/test_prof/autopilot/stack_prof/printer.rb +18 -0
- data/lib/test_prof/autopilot/stack_prof/profiling_executor.rb +27 -0
- data/lib/test_prof/autopilot/stack_prof/report.rb +74 -0
- data/lib/test_prof/autopilot/stack_prof/writer.rb +23 -0
- data/lib/test_prof/autopilot/version.rb +7 -0
- data/lib/test_prof/autopilot.rb +7 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 88be05ff8b53b5d4c831a5ed872348af97a21489a214a0fe14db3ad37976bd37
|
4
|
+
data.tar.gz: 7e1d10dfc1b62abdc435972a3842b6ab4e9ff3c0ee9508f627014f55e28772fc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b45afde7024c8fe293348f434a506008bd7f40ca188c262e8876db693f9b758c7982371eb273dad2e9a012727046ef00d92447683f405da947493904ce64c39d
|
7
|
+
data.tar.gz: d985c1d20353e05a468d46978300f99264cc1f2f2f5bc890fe93b167f17cdeb564decd0677694d1fbe1a03364aa5e26ce1f0e7e77bf74e2ed0969a61bd8691f7
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Vladimir Dementyev, Ruslan Shakirov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# TestProf Autopilot (PoC)
|
2
|
+
|
3
|
+
[TestProf][] has been used by many Ruby/Rails teams to optimize their test suites performance for a while.
|
4
|
+
|
5
|
+
Usually, it takes a decent amount of time to profile the test suite initially: we need run many profilers multiple times, tune configuration and sampling parameters. And we repeat this over and over again.
|
6
|
+
|
7
|
+
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
|
+
|
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.
|
22
|
+
|
23
|
+
Profiling plan is a Ruby file using a custom DSL to run profilers and access their reports:
|
24
|
+
|
25
|
+
Here is an example #2:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# This plan runs multiple test samples and collects StackProf data.
|
29
|
+
# The data is aggregated and the top-5 popular methods are displayed in the end.
|
30
|
+
#
|
31
|
+
# With the help of this plan, you can detect such problems as unnecessary logging/instrumentation in tests,
|
32
|
+
# inproper encryption settings, etc.
|
33
|
+
#
|
34
|
+
# NOTE: `aggregate` takes a block, runs it the specified number of times and merge the reports (i.e., agg_result = prev_result.merge(curr_result))
|
35
|
+
aggregate(3) { run :stackprof, sample: 100 }
|
36
|
+
|
37
|
+
# `report` returns the latest generated report (both `run` and `aggregate` set this value automatically)
|
38
|
+
# `#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)
|
41
|
+
```
|
42
|
+
|
43
|
+
And example #2:
|
44
|
+
|
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.
|
48
|
+
|
49
|
+
run :event_prof, event: "factory.create"
|
50
|
+
run :factory_prof, paths: report.paths
|
51
|
+
|
52
|
+
info report
|
53
|
+
```
|
54
|
+
|
55
|
+
### Notes on implementation
|
56
|
+
|
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:`).
|
59
|
+
|
60
|
+
## Installation
|
61
|
+
|
62
|
+
Adding to a gem:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# my-cool-gem.gemspec
|
66
|
+
Gem::Specification.new do |spec|
|
67
|
+
# ...
|
68
|
+
spec.add_dependency "test-prof-autopilot"
|
69
|
+
# ...
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
Or adding to your project:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# Gemfile
|
77
|
+
group :development, :test do
|
78
|
+
gem "test-prof-autopilot"
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
[TestProf]: https://test-prof.evilmartians.io/
|
data/bin/auto-test-prof
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_path = File.expand_path("../lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
5
|
+
|
6
|
+
require "test_prof/autopilot/cli"
|
7
|
+
|
8
|
+
begin
|
9
|
+
cli = TestProf::Autopilot::CLI.new
|
10
|
+
cli.run(ARGV)
|
11
|
+
rescue => e
|
12
|
+
raise e if $DEBUG
|
13
|
+
STDERR.puts e.message
|
14
|
+
STDERR.puts e.backtrace.join("\n") if ENV["DEBUG_TEST_PROF"] == "1"
|
15
|
+
exit 1
|
16
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "test_prof/autopilot/runner"
|
5
|
+
|
6
|
+
module TestProf
|
7
|
+
module Autopilot
|
8
|
+
class CLI
|
9
|
+
attr_reader :command, :plan_path
|
10
|
+
|
11
|
+
def run(args = ARGV)
|
12
|
+
optparser.parse!(args)
|
13
|
+
|
14
|
+
raise "Test command must be specified. See -h for options" unless command
|
15
|
+
|
16
|
+
raise "Plan path must be specified. See -h for options" unless plan_path
|
17
|
+
|
18
|
+
raise "Plan #{plan_path} doesn't exist" unless File.file?(plan_path)
|
19
|
+
|
20
|
+
Runner.invoke(plan_path, command)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def optparser
|
26
|
+
@optparser ||= OptionParser.new do |opts|
|
27
|
+
opts.banner = "Usage: auto-test-prof [options]"
|
28
|
+
|
29
|
+
opts.on("-v", "--version", "Print version") do
|
30
|
+
$stdout.puts TestProf::Autopilot::VERSION
|
31
|
+
exit 0
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-c COMMAND", "--command", "Command to run tests") do |val|
|
35
|
+
@command = val
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-i FILE", "--plan", "Path to test plan") do |val|
|
39
|
+
@plan_path = val
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module Autopilot
|
7
|
+
# Module is used for commands execution in child process.
|
8
|
+
module CommandExecutor
|
9
|
+
def execute(env, command)
|
10
|
+
Open3.popen2e(env, command) do |_stdin, stdout_and_stderr, _wait_thr|
|
11
|
+
while (line = stdout_and_stderr.gets)
|
12
|
+
Logging.log line
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module_function :execute
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
# Global configuration
|
6
|
+
class Configuration
|
7
|
+
class << self
|
8
|
+
def config
|
9
|
+
@config ||= new
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure
|
13
|
+
yield config
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :output,
|
18
|
+
:tmp_dir,
|
19
|
+
:artifacts_dir,
|
20
|
+
:plan_path,
|
21
|
+
:command
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@output = $stdout
|
25
|
+
@tmp_dir = "tmp/test_prof_autopilot"
|
26
|
+
@artifacts_dir = "test_prof_autopilot"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/autopilot/event_prof/printer"
|
4
|
+
require "test_prof/autopilot/event_prof/profiling_executor"
|
5
|
+
|
6
|
+
require "test_prof/autopilot/factory_prof/printer"
|
7
|
+
require "test_prof/autopilot/factory_prof/profiling_executor"
|
8
|
+
|
9
|
+
require "test_prof/autopilot/stack_prof/printer"
|
10
|
+
require "test_prof/autopilot/stack_prof/writer"
|
11
|
+
require "test_prof/autopilot/stack_prof/profiling_executor"
|
12
|
+
|
13
|
+
module TestProf
|
14
|
+
module Autopilot
|
15
|
+
# Module contains all available DSL instructions
|
16
|
+
module Dsl
|
17
|
+
# 'run' is used to start profiling
|
18
|
+
# profiler – uniq name of profiler; available profilers – :event_prof, :factory_prof, :stack_prof
|
19
|
+
# options; available options – :sample, :paths and :event ('event_prof' profiler only)
|
20
|
+
def run(profiler, **options)
|
21
|
+
Logging.log "Executing 'run' with profiler:#{profiler} and options:#{options}"
|
22
|
+
|
23
|
+
executor = Registry.fetch(:"#{profiler}_executor").new(options).start
|
24
|
+
|
25
|
+
@report = executor.report
|
26
|
+
end
|
27
|
+
|
28
|
+
# 'aggregate' is used to run one profiler several times and merge results
|
29
|
+
# supported profilers – 'stack_prof'
|
30
|
+
#
|
31
|
+
# example of using:
|
32
|
+
# aggregate(3) { run :stack_prof, sample: 100 }
|
33
|
+
def aggregate(number, &block)
|
34
|
+
raise ArgumentError, "Block is required!" unless block
|
35
|
+
|
36
|
+
agg_report = nil
|
37
|
+
|
38
|
+
number.times do
|
39
|
+
block.call
|
40
|
+
|
41
|
+
agg_report = agg_report.nil? ? report : agg_report.merge(report)
|
42
|
+
end
|
43
|
+
|
44
|
+
@report = agg_report
|
45
|
+
end
|
46
|
+
|
47
|
+
# 'info' prints report
|
48
|
+
# printable_object; available printable objects – 'report'
|
49
|
+
def info(printable_object)
|
50
|
+
Registry.fetch(:"#{printable_object.type}_printer").print_report(printable_object)
|
51
|
+
end
|
52
|
+
|
53
|
+
# 'save' writes report to file
|
54
|
+
def save(report, **options)
|
55
|
+
Registry.fetch(:"#{report.type}_writer").write_report(report, **options)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/ext/float_duration"
|
4
|
+
require "test_prof/ext/string_truncate"
|
5
|
+
|
6
|
+
module TestProf
|
7
|
+
module Autopilot
|
8
|
+
module EventProf
|
9
|
+
# Module is used for printing :event_prof report
|
10
|
+
module Printer
|
11
|
+
Registry.register(:event_prof_printer, self)
|
12
|
+
|
13
|
+
using FloatDuration
|
14
|
+
using StringTruncate
|
15
|
+
|
16
|
+
def print_report(report)
|
17
|
+
result = report.raw_report
|
18
|
+
msgs = []
|
19
|
+
|
20
|
+
msgs <<
|
21
|
+
<<~MSG
|
22
|
+
EventProf results for #{result["event"]}
|
23
|
+
|
24
|
+
Total time: #{result["total_time"].duration} of #{result["absolute_run_time"].duration} (#{result["time_percentage"]}%)
|
25
|
+
Total events: #{result["total_count"]}
|
26
|
+
|
27
|
+
Top #{result["top_count"]} slowest suites (by #{result["rank_by"]}):
|
28
|
+
MSG
|
29
|
+
|
30
|
+
result["groups"].each do |group|
|
31
|
+
msgs <<
|
32
|
+
<<~GROUP
|
33
|
+
#{group["description"].truncate} (#{group["location"]}) – #{group["time"].duration} (#{group["count"]} / #{group["examples"]}) of #{group["run_time"].duration} (#{group["time_percentage"]}%)
|
34
|
+
GROUP
|
35
|
+
end
|
36
|
+
|
37
|
+
Logging.log msgs.join
|
38
|
+
end
|
39
|
+
|
40
|
+
module_function :print_report
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/autopilot/profiling_executor/base"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module Autopilot
|
7
|
+
module EventProf
|
8
|
+
# Provides :event_prof specific validations, env and command building.
|
9
|
+
class ProfilingExecutor < ProfilingExecutor::Base
|
10
|
+
Registry.register(:event_prof_executor, self)
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
super
|
14
|
+
@profiler = :event_prof
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_profiler!
|
20
|
+
super
|
21
|
+
raise ArgumentError, "'event' option is required for 'event_prof' profiler" if @options[:event].nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_env
|
25
|
+
super.tap do |env|
|
26
|
+
env["EVENT_PROF"] = @options[:event]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "test_prof/autopilot/report_builder"
|
5
|
+
|
6
|
+
module TestProf
|
7
|
+
module Autopilot
|
8
|
+
module EventProf
|
9
|
+
# :event_prof report allows to add additional functionality
|
10
|
+
# for it's instances
|
11
|
+
class Report
|
12
|
+
Registry.register(:event_prof_report, self)
|
13
|
+
|
14
|
+
extend ReportBuilder
|
15
|
+
|
16
|
+
ARTIFACT_FILE = "event_prof_report.json"
|
17
|
+
|
18
|
+
attr_reader :type, :raw_report
|
19
|
+
|
20
|
+
def initialize(raw_report)
|
21
|
+
@type = :event_prof
|
22
|
+
@raw_report = raw_report
|
23
|
+
end
|
24
|
+
|
25
|
+
def paths
|
26
|
+
@raw_report["groups"].reduce("") { |paths, group| "#{paths} #{group["location"]}" }.strip
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
module FactoryProf
|
6
|
+
# Module is used for printing :factory_prof report
|
7
|
+
module Printer
|
8
|
+
Registry.register(:factory_prof_printer, self)
|
9
|
+
|
10
|
+
class PrinterError < StandardError; end
|
11
|
+
|
12
|
+
def print_report(report)
|
13
|
+
result = report.raw_report
|
14
|
+
|
15
|
+
raise PrinterError, result["error"] if result["error"]
|
16
|
+
|
17
|
+
msgs = []
|
18
|
+
|
19
|
+
msgs <<
|
20
|
+
<<~MSG
|
21
|
+
Factories usage
|
22
|
+
|
23
|
+
Total: #{result["total_count"]}
|
24
|
+
Total top-level: #{result["total_top_level_count"]}
|
25
|
+
Total time: #{format("%.4f", result["total_time"])}s
|
26
|
+
Total uniq factories: #{result["total_uniq_factories"]}
|
27
|
+
|
28
|
+
total top-level total time time per call top-level time name
|
29
|
+
MSG
|
30
|
+
|
31
|
+
result["stats"].each do |stat|
|
32
|
+
time_per_call = stat["total_time"] / stat["total_count"]
|
33
|
+
|
34
|
+
msgs << format("%8d %11d %13.4fs %17.4fs %18.4fs %18s", stat["total_count"], stat["top_level_count"], stat["total_time"], time_per_call, stat["top_level_time"], stat["name"])
|
35
|
+
end
|
36
|
+
|
37
|
+
Logging.log msgs.join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
module_function :print_report
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/autopilot/profiling_executor/base"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module Autopilot
|
7
|
+
module FactoryProf
|
8
|
+
# Provides :factory_prof specific validations, env and command building.
|
9
|
+
class ProfilingExecutor < ProfilingExecutor::Base
|
10
|
+
Registry.register(:factory_prof_executor, self)
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
super
|
14
|
+
@profiler = :factory_prof
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_env
|
20
|
+
super.tap do |env|
|
21
|
+
env["FPROF"] = "1"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "test_prof/autopilot/report_builder"
|
5
|
+
|
6
|
+
module TestProf
|
7
|
+
module Autopilot
|
8
|
+
module FactoryProf
|
9
|
+
# :factory_prof report allows to add additional functionality
|
10
|
+
# for it's instances
|
11
|
+
class Report
|
12
|
+
Registry.register(:factory_prof_report, self)
|
13
|
+
|
14
|
+
extend ReportBuilder
|
15
|
+
|
16
|
+
ARTIFACT_FILE = "factory_prof_report.json"
|
17
|
+
|
18
|
+
attr_reader :type, :raw_report
|
19
|
+
|
20
|
+
def initialize(raw_report)
|
21
|
+
@type = :factory_prof
|
22
|
+
@raw_report = raw_report
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
module Patches
|
6
|
+
# Monkey-patch for 'TestProf::EventProf::RSpecListener'.
|
7
|
+
# Redefined 'report' method provides writing artifact to the directory
|
8
|
+
# instead of printing report
|
9
|
+
module EventProfPatch
|
10
|
+
ARTIFACT_FILE = "event_prof_report.json"
|
11
|
+
|
12
|
+
def patch
|
13
|
+
TestProf::EventProf::RSpecListener.class_eval do
|
14
|
+
def report(profiler)
|
15
|
+
result = profiler.results
|
16
|
+
|
17
|
+
profiler_hash = {
|
18
|
+
event: profiler.event,
|
19
|
+
total_time: profiler.total_time,
|
20
|
+
absolute_run_time: profiler.absolute_run_time,
|
21
|
+
total_count: profiler.total_count,
|
22
|
+
top_count: profiler.top_count,
|
23
|
+
rank_by: profiler.rank_by,
|
24
|
+
time_percentage: time_percentage(profiler.total_time, profiler.absolute_run_time)
|
25
|
+
}
|
26
|
+
|
27
|
+
profiler_hash[:groups] = result[:groups].map do |group|
|
28
|
+
{
|
29
|
+
description: group[:id].top_level_description,
|
30
|
+
location: group[:id].metadata[:location],
|
31
|
+
time: group[:time],
|
32
|
+
count: group[:count],
|
33
|
+
examples: group[:examples],
|
34
|
+
run_time: group[:run_time],
|
35
|
+
time_percentage: time_percentage(group[:time], group[:run_time])
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
dir_path = FileUtils.mkdir_p(Configuration.config.tmp_dir)[0]
|
40
|
+
file_path = File.join(dir_path, ARTIFACT_FILE)
|
41
|
+
|
42
|
+
File.write(file_path, JSON.generate(profiler_hash))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module_function :patch
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
TestProf::Autopilot::Patches::EventProfPatch.patch
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
module Patches
|
6
|
+
# Monkey-patch for 'TestProf::FactoryProf::Printers::Simple'.
|
7
|
+
# Redefined 'report' method provides writing artifact to the directory
|
8
|
+
# instead of printing report
|
9
|
+
module FactoryProfPatch
|
10
|
+
ARTIFACT_FILE = "factory_prof_report.json"
|
11
|
+
|
12
|
+
def patch
|
13
|
+
TestProf::FactoryProf::Printers::Simple.module_eval do
|
14
|
+
def self.dump(result, **)
|
15
|
+
profiler_hash =
|
16
|
+
if result.raw_stats == {}
|
17
|
+
{
|
18
|
+
error: "No factories detected"
|
19
|
+
}
|
20
|
+
else
|
21
|
+
{
|
22
|
+
total_count: result.stats.sum { |stat| stat[:total_count] },
|
23
|
+
total_top_level_count: result.stats.sum { |stat| stat[:top_level_count] },
|
24
|
+
total_time: result.stats.sum { |stat| stat[:top_level_time] },
|
25
|
+
total_uniq_factories: result.stats.uniq { |stat| stat[:name] }.count,
|
26
|
+
stats: result.stats
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
dir_path = FileUtils.mkdir_p(Configuration.config.tmp_dir)[0]
|
31
|
+
file_path = File.join(dir_path, ARTIFACT_FILE)
|
32
|
+
|
33
|
+
File.write(file_path, JSON.generate(profiler_hash))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module_function :patch
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
TestProf::Autopilot::Patches::FactoryProfPatch.patch
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
module Patches
|
6
|
+
module StackProfPatch
|
7
|
+
ARTIFACT_FILE = "stack_prof_report.dump"
|
8
|
+
|
9
|
+
def patch
|
10
|
+
TestProf::StackProf.module_eval do
|
11
|
+
private
|
12
|
+
|
13
|
+
def self.build_path(_name)
|
14
|
+
dir_path = FileUtils.mkdir_p(Configuration.config.tmp_dir)[0]
|
15
|
+
File.join(dir_path, ARTIFACT_FILE)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module_function :patch
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
TestProf::Autopilot::Patches::StackProfPatch.patch
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/autopilot/command_executor"
|
4
|
+
require "test_prof/autopilot/event_prof/report"
|
5
|
+
require "test_prof/autopilot/factory_prof/report"
|
6
|
+
require "test_prof/autopilot/stack_prof/report"
|
7
|
+
|
8
|
+
module TestProf
|
9
|
+
module Autopilot
|
10
|
+
module ProfilingExecutor
|
11
|
+
# Provides base command and env variables building;
|
12
|
+
# Calls command executor;
|
13
|
+
# Builds report.
|
14
|
+
class Base
|
15
|
+
attr_reader :report
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
validate_profiler!
|
23
|
+
|
24
|
+
execute
|
25
|
+
build_report
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate_profiler!
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute
|
36
|
+
env = build_env
|
37
|
+
command = build_command
|
38
|
+
|
39
|
+
CommandExecutor.execute(env, command)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_env
|
43
|
+
env = {}
|
44
|
+
|
45
|
+
env["SAMPLE"] = @options[:sample].to_s if @options[:sample]
|
46
|
+
env
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_command
|
50
|
+
return Configuration.config.command if @options[:paths].nil?
|
51
|
+
|
52
|
+
"#{Configuration.config.command} #{@options[:paths]}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_report
|
56
|
+
@report = Registry.fetch(:"#{@profiler}_report").build
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
# Global registry
|
6
|
+
class Registry
|
7
|
+
@items = {}
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def register(key, klass)
|
11
|
+
@items[key] = klass
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch(key)
|
15
|
+
@items.fetch(key)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
# Common module that extends reports classes
|
6
|
+
module ReportBuilder
|
7
|
+
ARTIFACT_MISSING_HINT = "Have you required 'test_prof/autopilot' to your code? "
|
8
|
+
|
9
|
+
def build
|
10
|
+
report = JSON.parse(fetch_report)
|
11
|
+
|
12
|
+
new(report)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def fetch_report
|
18
|
+
file_path = File.join(Configuration.config.tmp_dir, self::ARTIFACT_FILE)
|
19
|
+
File.read(file_path)
|
20
|
+
rescue Errno::ENOENT => e
|
21
|
+
e.message.prepend(ARTIFACT_MISSING_HINT)
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/autopilot/configuration"
|
4
|
+
require "test_prof/autopilot/registry"
|
5
|
+
require "test_prof/autopilot/logging"
|
6
|
+
require "test_prof/autopilot/dsl"
|
7
|
+
require "fileutils"
|
8
|
+
|
9
|
+
module TestProf
|
10
|
+
module Autopilot
|
11
|
+
class Runner
|
12
|
+
prepend Dsl
|
13
|
+
|
14
|
+
attr_reader :report
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def invoke(plan_path, command)
|
18
|
+
Configuration.configure do |config|
|
19
|
+
config.plan_path = plan_path
|
20
|
+
config.command = command
|
21
|
+
end
|
22
|
+
|
23
|
+
Logging.log "Reading #{plan_path}..."
|
24
|
+
|
25
|
+
new.instance_eval(File.read(Configuration.config.plan_path))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
module StackProf
|
6
|
+
# Module is used for printing :stack_prof report
|
7
|
+
module Printer
|
8
|
+
Registry.register(:stack_prof_printer, self)
|
9
|
+
|
10
|
+
def print_report(report)
|
11
|
+
report.print_text
|
12
|
+
end
|
13
|
+
|
14
|
+
module_function :print_report
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/autopilot/profiling_executor/base"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module Autopilot
|
7
|
+
module StackProf
|
8
|
+
# Provides :stack_prof specific validations, env and command building.
|
9
|
+
class ProfilingExecutor < ProfilingExecutor::Base
|
10
|
+
Registry.register(:stack_prof_executor, self)
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
super
|
14
|
+
@profiler = :stack_prof
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_env
|
20
|
+
super.tap do |env|
|
21
|
+
env["TEST_STACK_PROF"] = "1"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stackprof/report"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module Autopilot
|
7
|
+
module StackProf
|
8
|
+
class Report < ::StackProf::Report
|
9
|
+
Registry.register(:stack_prof_report, self)
|
10
|
+
|
11
|
+
extend ReportBuilder
|
12
|
+
|
13
|
+
ARTIFACT_FILE = "stack_prof_report.dump"
|
14
|
+
|
15
|
+
attr_reader :type
|
16
|
+
|
17
|
+
def initialize(data)
|
18
|
+
@type = :stack_prof
|
19
|
+
super(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.build
|
23
|
+
new(Marshal.load(fetch_report))
|
24
|
+
end
|
25
|
+
|
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
|
42
|
+
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)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
else
|
50
|
+
hash[id] = f1[id]
|
51
|
+
end
|
52
|
+
hash
|
53
|
+
end
|
54
|
+
|
55
|
+
d1, d2 = data, other.data
|
56
|
+
data = {
|
57
|
+
version: version,
|
58
|
+
mode: d1[:mode],
|
59
|
+
interval: d1[:interval],
|
60
|
+
samples: d1[:samples] + d2[:samples],
|
61
|
+
gc_samples: d1[:gc_samples] + d2[:gc_samples],
|
62
|
+
missed_samples: d1[:missed_samples] + d2[:missed_samples],
|
63
|
+
metadata: d1[:metadata].merge(d2[:metadata]),
|
64
|
+
frames: frames,
|
65
|
+
raw: d1[:raw] + d2[:raw],
|
66
|
+
raw_timestamp_deltas: d1[:raw_timestamp_deltas] + d2[:raw_timestamp_deltas]
|
67
|
+
}
|
68
|
+
|
69
|
+
self.class.new(data)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module Autopilot
|
5
|
+
module StackProf
|
6
|
+
# Module is used for writing :stack_prof report in different formats
|
7
|
+
module Writer
|
8
|
+
Registry.register(:stack_prof_writer, self)
|
9
|
+
|
10
|
+
ARTIFACT_FILE = "stack_prof_report"
|
11
|
+
|
12
|
+
def write_report(report, file_name: ARTIFACT_FILE)
|
13
|
+
dir_path = FileUtils.mkdir_p(Configuration.config.artifacts_dir)[0]
|
14
|
+
file_path = File.join(dir_path, file_name + ".json")
|
15
|
+
|
16
|
+
File.write(file_path, JSON.generate(report.data))
|
17
|
+
end
|
18
|
+
|
19
|
+
module_function :write_report
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test-prof"
|
4
|
+
require "test_prof/autopilot/configuration"
|
5
|
+
require "test_prof/autopilot/patches/event_prof_patch"
|
6
|
+
require "test_prof/autopilot/patches/factory_prof_patch"
|
7
|
+
require "test_prof/autopilot/patches/stack_prof_patch"
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: test-prof-autopilot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ruslan Shakirov
|
8
|
+
- Vladimir Dementyev
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2022-06-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: test-prof
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: stackprof
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.2.9
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.2.9
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '1.15'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '1.15'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rake
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '13.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '13.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rspec
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '3.10'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '3.10'
|
84
|
+
description: Automatic TestProf runner
|
85
|
+
email:
|
86
|
+
- ruslan@shakirov.dev
|
87
|
+
- dementiev.vm@gmail.com
|
88
|
+
executables:
|
89
|
+
- auto-test-prof
|
90
|
+
extensions: []
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- CHANGELOG.md
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- bin/auto-test-prof
|
97
|
+
- lib/test-prof-autopilot.rb
|
98
|
+
- lib/test_prof/autopilot.rb
|
99
|
+
- lib/test_prof/autopilot/cli.rb
|
100
|
+
- lib/test_prof/autopilot/command_executor.rb
|
101
|
+
- lib/test_prof/autopilot/configuration.rb
|
102
|
+
- lib/test_prof/autopilot/dsl.rb
|
103
|
+
- lib/test_prof/autopilot/event_prof/printer.rb
|
104
|
+
- lib/test_prof/autopilot/event_prof/profiling_executor.rb
|
105
|
+
- lib/test_prof/autopilot/event_prof/report.rb
|
106
|
+
- lib/test_prof/autopilot/factory_prof/printer.rb
|
107
|
+
- lib/test_prof/autopilot/factory_prof/profiling_executor.rb
|
108
|
+
- lib/test_prof/autopilot/factory_prof/report.rb
|
109
|
+
- lib/test_prof/autopilot/logging.rb
|
110
|
+
- lib/test_prof/autopilot/patches/event_prof_patch.rb
|
111
|
+
- lib/test_prof/autopilot/patches/factory_prof_patch.rb
|
112
|
+
- lib/test_prof/autopilot/patches/stack_prof_patch.rb
|
113
|
+
- lib/test_prof/autopilot/profiling_executor/base.rb
|
114
|
+
- lib/test_prof/autopilot/registry.rb
|
115
|
+
- lib/test_prof/autopilot/report_builder.rb
|
116
|
+
- lib/test_prof/autopilot/runner.rb
|
117
|
+
- lib/test_prof/autopilot/stack_prof/printer.rb
|
118
|
+
- lib/test_prof/autopilot/stack_prof/profiling_executor.rb
|
119
|
+
- lib/test_prof/autopilot/stack_prof/report.rb
|
120
|
+
- lib/test_prof/autopilot/stack_prof/writer.rb
|
121
|
+
- lib/test_prof/autopilot/version.rb
|
122
|
+
homepage: http://github.com/test-prof/test-prof-autopilot
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '2.5'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubygems_version: 3.3.15
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Automatic TestProf runner
|
145
|
+
test_files: []
|