test-prof-autopilot 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|