Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/spec/reports/
/tmp/
/vendor/bundle
/vendor/speedscope
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ gemspec

gem "activejob"
gem "rake", "~> 13.0"
gem "rspec"
gem "rubyzip"
gem "sidekiq"
gem "standard"
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ GEM
logger (>= 1.7.0)
rack (>= 3.2.0)
redis-client (>= 0.26.0)
rubyzip (3.2.2)
stackprof (0.2.28)
standard (1.35.1)
language_server-protocol (~> 3.17.0.2)
Expand All @@ -116,6 +117,7 @@ DEPENDENCIES
activejob
rake (~> 13.0)
rspec
rubyzip
sidekiq
singed!
standard
Expand Down
51 changes: 51 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,55 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "open-uri"
require "fileutils"
require "tmpdir"
require "zip"
require_relative "lib/singed/speedscope"

namespace :speedscope do
destination_dir = File.expand_path("vendor/speedscope", __dir__)

desc "Download and unpack speedscope into vendor/speedscope"
task vendor: destination_dir

directory destination_dir do
version = Singed::Speedscope::VERSION
url = "https://github.com/jlfwong/speedscope/releases/download/v#{version}/speedscope-#{version}.zip"

unzip_dir = File.expand_path("..", destination_dir) # speedscope dir is in the archive
FileUtils.mkdir_p(destination_dir)

tmp_zip = File.join(Dir.tmpdir, "speedscope-#{version}.zip")

puts "Downloading speedscope from #{url}"
URI.parse(url).open do |remote|
File.open(tmp_zip, "wb") do |file|
IO.copy_stream(remote, file)
end
end

puts "Vendoring speedscope into #{unzip_dir}"
Zip::File.open(tmp_zip) do |zip_file|
zip_file.each do |entry|
destination = File.join(unzip_dir, entry.name)
if entry.directory?
FileUtils.mkdir_p(destination)
else
FileUtils.mkdir_p(File.dirname(destination))
entry.extract(destination_directory: unzip_dir)
end
end
end
end

desc "Remove the unpacked speedscope directory"
task :clobber do
FileUtils.rm_rf(destination_dir)
end
end

Rake::Task[:build].enhance ["speedscope:vendor"]
Rake::Task[:clobber].enhance ["speedscope:clobber"]

task default: %i[]
1 change: 1 addition & 0 deletions lib/singed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def profiling?
autoload :Flamegraph, "singed/flamegraph"
autoload :Report, "singed/report"
autoload :RackMiddleware, "singed/rack_middleware"
autoload :Speedscope, "singed/speedscope"
end

require "singed/kernel_ext"
Expand Down
4 changes: 2 additions & 2 deletions lib/singed/flamegraph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ def save
end

def open
system open_command
Singed::Speedscope.open(@filename)
end

def open_command
@open_command ||= "npx speedscope #{@filename}"
Singed::Speedscope.open_command(@filename)
end

def self.generate_filename(label: nil, time: Time.now) # rubocop:disable Rails/TimeZone
Expand Down
1 change: 0 additions & 1 deletion lib/singed/kernel_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ def flamegraph(label = nil, open: true, ignore_gc: false, interval: 1000, io: $s
bright_red = "\e[91m"
none = "\e[0m"
if open
# use npx, so we don't have to add it as a dependency
io.puts "🔥📈 #{bright_red}Captured flamegraph, opening with#{none}: #{fg.open_command}"
fg.open
else
Expand Down
81 changes: 81 additions & 0 deletions lib/singed/speedscope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

require "rbconfig"
require "tmpdir"

module Singed
module Speedscope
# Take latest version from https://github.com/jlfwong/speedscope/releases
# that have ZIP archive with self-contained version published
VERSION = "1.24.0"

class << self
def bundled_index_html
File.join(File.expand_path("../..", __dir__), "vendor", "speedscope", "index.html")
end

def open_command(profile_path)
if File.exist?(bundled_index_html)
"#{os_open_command} file://#{bundled_index_html}#localProfilePath=#{profile_path}"
else
"npx speedscope #{profile_path}"
end
end

def open(profile_path)
profile_path = profile_path.to_s

if File.exist?(bundled_index_html)
open_with_bundled_speedscope(profile_path)
else
open_with_npx(profile_path)
end
end

private

def open_with_npx(profile_path)
system("npx", "speedscope", profile_path)
end

# Based on speedscope CLI code (MIT license)
# See https://github.com/jlfwong/speedscope/blob/3613918de0dd55a263d0d04f85b0c8c2039c7bee/bin/cli.mjs
def open_with_bundled_speedscope(profile_path)
source_buffer = File.binread(profile_path)
filename = File.basename(profile_path)

source_base64 = [source_buffer].pack("m0")
js_source = "speedscope.loadFileFromBase64(#{filename.inspect}, #{source_base64.inspect})"

file_prefix = "speedscope-#{Time.now.to_i}-#{Process.pid}"
js_path = File.join(Dir.tmpdir, "#{file_prefix}.js")
File.write(js_path, js_source)

url_to_open = "file://#{File.expand_path(bundled_index_html)}#localProfilePath=#{js_path}"

# See https://github.com/jlfwong/speedscope/blob/3613918de0dd55a263d0d04f85b0c8c2039c7bee/bin/cli.mjs#L96-L105
host_os = RbConfig::CONFIG["host_os"]
if host_os =~ /mswin|mingw|cygwin/ || host_os =~ /darwin/
html_path = File.join(Dir.tmpdir, "#{file_prefix}.html")
File.write(html_path, "<script>window.location=#{url_to_open.inspect}</script>")
url_to_open = "file://#{html_path}"
end

system os_open_command, url_to_open
end

def os_open_command
case host_os = RbConfig::CONFIG["host_os"]
when /mswin|mingw|cygwin/
"start"
when /darwin/
"open"
when /linux|bsd/
"xdg-open"
else
raise "Unsupported OS to open browser: #{host_os}"
end
end
end
end
end
5 changes: 1 addition & 4 deletions singed.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,10 @@ Gem::Specification.new do |spec|
"homepage_uri" => "https://github.com/rubyatscale/singed"
}

spec.files = Dir["README.md", "*.gemspec", "lib/**/*", "exe/**/*"]
spec.files = Dir["README.md", "*.gemspec", "lib/**/*", "exe/**/*", "vendor/speedscope/**/*"]
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "stackprof", ">= 0.2.13"

spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec"
end
83 changes: 83 additions & 0 deletions spec/singed/speedscope_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

require "tempfile"

RSpec.describe Singed::Speedscope do
describe ".open" do
let(:profile_path) do
Tempfile.new(["profile", ".json"]).tap do |file|
file.write("{}")
file.flush
end.path
end

context "when bundled speedscope exists" do
before do
allow(File).to receive(:exist?).with(described_class.bundled_index_html).and_return(true)
end

it "opens with bundled speedscope" do
allow(described_class).to receive(:system).and_return(true)

described_class.open(profile_path)

expect(described_class).to have_received(:system).with(described_class.send(:os_open_command), %r{\Afile://})
end
end

context "when bundled speedscope does not exist" do
before do
allow(File).to receive(:exist?).with(described_class.bundled_index_html).and_return(false)
end

it "opens with npx speedscope" do
allow(described_class).to receive(:system).and_return(true)

described_class.open(profile_path)

expect(described_class).to have_received(:system).with("npx", "speedscope", profile_path)
end
end
end

describe ".os_open_command" do
it "returns a command and does not raise" do
expect { described_class.send(:os_open_command) }.not_to raise_error
expect(described_class.send(:os_open_command)).to match(/\A(start|open|xdg-open)\z/)
end

context "when host_os is stubbed" do
subject { described_class.send(:os_open_command) }

before do
allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return(stubbed_os)
end

context "on Windows" do
let(:stubbed_os) { "mingw32" }

it { is_expected.to eq("start") }
end

context "on MacOS" do
let(:stubbed_os) { "darwin22.0" }

it { is_expected.to eq("open") }
end

context "on Linux" do
let(:stubbed_os) { "linux-gnu" }

it { is_expected.to eq("xdg-open") }
end

context "on unsupported OS" do
let(:stubbed_os) { "unknown-os" }

it "raises error" do
expect { subject }.to raise_error(RuntimeError, /unknown-os/)
end
end
end
end
end
Loading