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.
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: []