diff --git a/lib/app_profiler.rb b/lib/app_profiler.rb index ff79bdb..8fa484d 100644 --- a/lib/app_profiler.rb +++ b/lib/app_profiler.rb @@ -30,6 +30,7 @@ module Storage module Viewer autoload :BaseViewer, "app_profiler/viewer/base_viewer" autoload :SpeedscopeViewer, "app_profiler/viewer/speedscope_viewer" + autoload :FirefoxViewer, "app_profiler/viewer/firefox_viewer" autoload :BaseMiddleware, "app_profiler/viewer/base_middleware" autoload :SpeedscopeRemoteViewer, "app_profiler/viewer/speedscope_remote_viewer" autoload :FirefoxRemoteViewer, "app_profiler/viewer/firefox_remote_viewer" @@ -125,7 +126,7 @@ def stackprof_viewer end def vernier_viewer - @@vernier_viewer ||= Viewer::FirefoxRemoteViewer # rubocop:disable Style/ClassVars + @@vernier_viewer ||= Viewer::FirefoxViewer # rubocop:disable Style/ClassVars end def profile_sampler_enabled=(value) diff --git a/lib/app_profiler/exec.rb b/lib/app_profiler/exec.rb new file mode 100644 index 0000000..9daf340 --- /dev/null +++ b/lib/app_profiler/exec.rb @@ -0,0 +1,35 @@ +module AppProfiler + module Exec # :nodoc: + protected + + def valid_commands + raise NotImplementedError + end + + def ensure_command_valid(command) + unless valid_command?(command) + raise YarnError, "Illegal command: #{command.join(" ")}." + end + end + + def valid_command?(command) + valid_commands.any? do |valid_command| + next unless valid_command.size == command.size + + valid_command.zip(command).all? do |valid_part, part| + part.match?(valid_part) + end + end + end + + def exec(*command, silent: false, environment: {}) + ensure_command_valid(command) + + if silent + system(environment, *command, out: File::NULL).tap { |return_code| yield unless return_code } + else + system(environment, *command).tap { |return_code| yield unless return_code } + end + end + end +end diff --git a/lib/app_profiler/viewer/firefox_viewer.rb b/lib/app_profiler/viewer/firefox_viewer.rb new file mode 100644 index 0000000..d3c023d --- /dev/null +++ b/lib/app_profiler/viewer/firefox_viewer.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "app_profiler/exec" + +module AppProfiler + module Viewer + class FirefoxViewer < BaseViewer + include Exec + + CHILD_PIDS = [] + + at_exit { sleep(1) until CHILD_PIDS.none? } + + trap("INT") do + CHILD_PIDS.delete_if { |pid| Process.kill("INT", pid) } + sleep(0.5) # Wait for webrick logs + end + + class ProfileViewerError < StandardError; end + + VALID_COMMANDS = [ + ["which", "profile-viewer"], + ["gem", "install", "profile-viewer"], + ["profile-viewer", /.*\.json/], + ] + private_constant(:VALID_COMMANDS) + + class << self + def view(profile, params = {}) + new(profile).view(**params) + end + end + + def valid_commands + VALID_COMMANDS + end + + def initialize(profile) + super() + @profile = profile + end + + def view(_params = {}) + profile_viewer(@profile.file.to_s) + end + + private + + def setup_profile_viewer + exec("which", "profile-viewer", silent: true) do + gem_install("profile_viewer") + end + @profile_viewer_initialized = true + end + + def profile_viewer_setup + @profile_viewer_initialized || false + end + + def gem_install(gem) + exec("gem", "install", gem) do + raise ProfileViewerError, "Failed to run gem install #{gem}." + end + end + + def profile_viewer(path) + setup_profile_viewer unless profile_viewer_setup + + CHILD_PIDS << fork do + Bundler.with_unbundled_env do + exec("profile-viewer", path) { AppProfiler.logger.info("Exiting") } + end + end + end + end + end +end diff --git a/lib/app_profiler/yarn/command.rb b/lib/app_profiler/yarn/command.rb index 2002758..f0c989b 100644 --- a/lib/app_profiler/yarn/command.rb +++ b/lib/app_profiler/yarn/command.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true +require "app_profiler/exec" + module AppProfiler module Yarn module Command + include Exec + class YarnError < StandardError; end VALID_COMMANDS = [ @@ -17,6 +21,10 @@ class YarnError < StandardError; end private_constant(:VALID_COMMANDS) + def valid_commands + VALID_COMMANDS + end + def yarn(command, *options) setup_yarn unless yarn_setup @@ -41,22 +49,6 @@ def yarn_setup=(state) private - def ensure_command_valid(command) - unless valid_command?(command) - raise YarnError, "Illegal command: #{command.join(" ")}." - end - end - - def valid_command?(command) - VALID_COMMANDS.any? do |valid_command| - next unless valid_command.size == command.size - - valid_command.zip(command).all? do |valid_part, part| - part.match?(valid_part) - end - end - end - def ensure_yarn_installed exec("which", "yarn", silent: true) do raise( @@ -73,16 +65,6 @@ def ensure_yarn_installed def package_json_exists? AppProfiler.root.join("package.json").exist? end - - def exec(*command, silent: false) - ensure_command_valid(command) - - if silent - system(*command, out: File::NULL).tap { |return_code| yield unless return_code } - else - system(*command).tap { |return_code| yield unless return_code } - end - end end end end