test-prof-autopilot 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +82 -0
  5. data/bin/auto-test-prof +16 -0
  6. data/lib/test-prof-autopilot.rb +3 -0
  7. data/lib/test_prof/autopilot/cli.rb +45 -0
  8. data/lib/test_prof/autopilot/command_executor.rb +20 -0
  9. data/lib/test_prof/autopilot/configuration.rb +30 -0
  10. data/lib/test_prof/autopilot/dsl.rb +59 -0
  11. data/lib/test_prof/autopilot/event_prof/printer.rb +44 -0
  12. data/lib/test_prof/autopilot/event_prof/profiling_executor.rb +32 -0
  13. data/lib/test_prof/autopilot/event_prof/report.rb +31 -0
  14. data/lib/test_prof/autopilot/factory_prof/printer.rb +44 -0
  15. data/lib/test_prof/autopilot/factory_prof/profiling_executor.rb +27 -0
  16. data/lib/test_prof/autopilot/factory_prof/report.rb +27 -0
  17. data/lib/test_prof/autopilot/logging.rb +13 -0
  18. data/lib/test_prof/autopilot/patches/event_prof_patch.rb +53 -0
  19. data/lib/test_prof/autopilot/patches/factory_prof_patch.rb +44 -0
  20. data/lib/test_prof/autopilot/patches/stack_prof_patch.rb +26 -0
  21. data/lib/test_prof/autopilot/profiling_executor/base.rb +61 -0
  22. data/lib/test_prof/autopilot/registry.rb +20 -0
  23. data/lib/test_prof/autopilot/report_builder.rb +26 -0
  24. data/lib/test_prof/autopilot/runner.rb +30 -0
  25. data/lib/test_prof/autopilot/stack_prof/printer.rb +18 -0
  26. data/lib/test_prof/autopilot/stack_prof/profiling_executor.rb +27 -0
  27. data/lib/test_prof/autopilot/stack_prof/report.rb +74 -0
  28. data/lib/test_prof/autopilot/stack_prof/writer.rb +23 -0
  29. data/lib/test_prof/autopilot/version.rb +7 -0
  30. data/lib/test_prof/autopilot.rb +7 -0
  31. 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
@@ -0,0 +1,5 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ [@palkan]: https://github.com/palkan
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/
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/autopilot/version"
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module Autopilot
5
+ module Logging
6
+ def log(message)
7
+ Configuration.config.output.puts(message)
8
+ end
9
+
10
+ module_function :log
11
+ end
12
+ end
13
+ 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
+ module TestProf
4
+ module Autopilot # :nodoc:
5
+ VERSION = "0.0.2"
6
+ end
7
+ 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: []