Skip to content

Commit 05a2571

Browse files
authored
Merge pull request #56 from Envek/package-speedscope
Vendor speedscope with gem
2 parents a032da1 + ab8444a commit 05a2571

10 files changed

Lines changed: 224 additions & 7 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
/spec/reports/
88
/tmp/
99
/vendor/bundle
10+
/vendor/speedscope

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ gemspec
77

88
gem "activejob"
99
gem "rake", "~> 13.0"
10+
gem "rspec"
11+
gem "rubyzip"
1012
gem "sidekiq"
1113
gem "standard"

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ GEM
9191
logger (>= 1.7.0)
9292
rack (>= 3.2.0)
9393
redis-client (>= 0.26.0)
94+
rubyzip (3.2.2)
9495
stackprof (0.2.28)
9596
standard (1.35.1)
9697
language_server-protocol (~> 3.17.0.2)
@@ -116,6 +117,7 @@ DEPENDENCIES
116117
activejob
117118
rake (~> 13.0)
118119
rspec
120+
rubyzip
119121
sidekiq
120122
singed!
121123
standard

Rakefile

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,55 @@
11
# frozen_string_literal: true
22

33
require "bundler/gem_tasks"
4+
require "open-uri"
5+
require "fileutils"
6+
require "tmpdir"
7+
require "zip"
8+
require_relative "lib/singed/speedscope"
9+
10+
namespace :speedscope do
11+
destination_dir = File.expand_path("vendor/speedscope", __dir__)
12+
13+
desc "Download and unpack speedscope into vendor/speedscope"
14+
task vendor: destination_dir
15+
16+
directory destination_dir do
17+
version = Singed::Speedscope::VERSION
18+
url = "https://github.com/jlfwong/speedscope/releases/download/v#{version}/speedscope-#{version}.zip"
19+
20+
unzip_dir = File.expand_path("..", destination_dir) # speedscope dir is in the archive
21+
FileUtils.mkdir_p(destination_dir)
22+
23+
tmp_zip = File.join(Dir.tmpdir, "speedscope-#{version}.zip")
24+
25+
puts "Downloading speedscope from #{url}"
26+
URI.parse(url).open do |remote|
27+
File.open(tmp_zip, "wb") do |file|
28+
IO.copy_stream(remote, file)
29+
end
30+
end
31+
32+
puts "Vendoring speedscope into #{unzip_dir}"
33+
Zip::File.open(tmp_zip) do |zip_file|
34+
zip_file.each do |entry|
35+
destination = File.join(unzip_dir, entry.name)
36+
if entry.directory?
37+
FileUtils.mkdir_p(destination)
38+
else
39+
FileUtils.mkdir_p(File.dirname(destination))
40+
entry.extract(destination_directory: unzip_dir)
41+
end
42+
end
43+
end
44+
end
45+
46+
desc "Remove the unpacked speedscope directory"
47+
task :clobber do
48+
FileUtils.rm_rf(destination_dir)
49+
end
50+
end
51+
52+
Rake::Task[:build].enhance ["speedscope:vendor"]
53+
Rake::Task[:clobber].enhance ["speedscope:clobber"]
54+
455
task default: %i[]

lib/singed.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def profiling?
7171
autoload :Flamegraph, "singed/flamegraph"
7272
autoload :Report, "singed/report"
7373
autoload :RackMiddleware, "singed/rack_middleware"
74+
autoload :Speedscope, "singed/speedscope"
7475
end
7576

7677
require "singed/kernel_ext"

lib/singed/flamegraph.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ def save
6262
end
6363

6464
def open
65-
system open_command
65+
Singed::Speedscope.open(@filename)
6666
end
6767

6868
def open_command
69-
@open_command ||= "npx speedscope #{@filename}"
69+
Singed::Speedscope.open_command(@filename)
7070
end
7171

7272
def self.generate_filename(label: nil, time: Time.now) # rubocop:disable Rails/TimeZone

lib/singed/kernel_ext.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ def flamegraph(label = nil, open: true, ignore_gc: false, interval: 1000, io: $s
88
bright_red = "\e[91m"
99
none = "\e[0m"
1010
if open
11-
# use npx, so we don't have to add it as a dependency
1211
io.puts "🔥📈 #{bright_red}Captured flamegraph, opening with#{none}: #{fg.open_command}"
1312
fg.open
1413
else

lib/singed/speedscope.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
require "rbconfig"
4+
require "tmpdir"
5+
6+
module Singed
7+
module Speedscope
8+
# Take latest version from https://github.com/jlfwong/speedscope/releases
9+
# that have ZIP archive with self-contained version published
10+
VERSION = "1.24.0"
11+
12+
class << self
13+
def bundled_index_html
14+
File.join(File.expand_path("../..", __dir__), "vendor", "speedscope", "index.html")
15+
end
16+
17+
def open_command(profile_path)
18+
if File.exist?(bundled_index_html)
19+
"#{os_open_command} file://#{bundled_index_html}#localProfilePath=#{profile_path}"
20+
else
21+
"npx speedscope #{profile_path}"
22+
end
23+
end
24+
25+
def open(profile_path)
26+
profile_path = profile_path.to_s
27+
28+
if File.exist?(bundled_index_html)
29+
open_with_bundled_speedscope(profile_path)
30+
else
31+
open_with_npx(profile_path)
32+
end
33+
end
34+
35+
private
36+
37+
def open_with_npx(profile_path)
38+
system("npx", "speedscope", profile_path)
39+
end
40+
41+
# Based on speedscope CLI code (MIT license)
42+
# See https://github.com/jlfwong/speedscope/blob/3613918de0dd55a263d0d04f85b0c8c2039c7bee/bin/cli.mjs
43+
def open_with_bundled_speedscope(profile_path)
44+
source_buffer = File.binread(profile_path)
45+
filename = File.basename(profile_path)
46+
47+
source_base64 = [source_buffer].pack("m0")
48+
js_source = "speedscope.loadFileFromBase64(#{filename.inspect}, #{source_base64.inspect})"
49+
50+
file_prefix = "speedscope-#{Time.now.to_i}-#{Process.pid}"
51+
js_path = File.join(Dir.tmpdir, "#{file_prefix}.js")
52+
File.write(js_path, js_source)
53+
54+
url_to_open = "file://#{File.expand_path(bundled_index_html)}#localProfilePath=#{js_path}"
55+
56+
# See https://github.com/jlfwong/speedscope/blob/3613918de0dd55a263d0d04f85b0c8c2039c7bee/bin/cli.mjs#L96-L105
57+
host_os = RbConfig::CONFIG["host_os"]
58+
if host_os =~ /mswin|mingw|cygwin/ || host_os =~ /darwin/
59+
html_path = File.join(Dir.tmpdir, "#{file_prefix}.html")
60+
File.write(html_path, "<script>window.location=#{url_to_open.inspect}</script>")
61+
url_to_open = "file://#{html_path}"
62+
end
63+
64+
system os_open_command, url_to_open
65+
end
66+
67+
def os_open_command
68+
case host_os = RbConfig::CONFIG["host_os"]
69+
when /mswin|mingw|cygwin/
70+
"start"
71+
when /darwin/
72+
"open"
73+
when /linux|bsd/
74+
"xdg-open"
75+
else
76+
raise "Unsupported OS to open browser: #{host_os}"
77+
end
78+
end
79+
end
80+
end
81+
end

singed.gemspec

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@ Gem::Specification.new do |spec|
1616
"homepage_uri" => "https://github.com/rubyatscale/singed"
1717
}
1818

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

2424
spec.add_dependency "stackprof", ">= 0.2.13"
25-
26-
spec.add_development_dependency "rake", "~> 13.0"
27-
spec.add_development_dependency "rspec"
2825
end

spec/singed/speedscope_spec.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require "tempfile"
4+
5+
RSpec.describe Singed::Speedscope do
6+
describe ".open" do
7+
let(:profile_path) do
8+
Tempfile.new(["profile", ".json"]).tap do |file|
9+
file.write("{}")
10+
file.flush
11+
end.path
12+
end
13+
14+
context "when bundled speedscope exists" do
15+
before do
16+
allow(File).to receive(:exist?).with(described_class.bundled_index_html).and_return(true)
17+
end
18+
19+
it "opens with bundled speedscope" do
20+
allow(described_class).to receive(:system).and_return(true)
21+
22+
described_class.open(profile_path)
23+
24+
expect(described_class).to have_received(:system).with(described_class.send(:os_open_command), %r{\Afile://})
25+
end
26+
end
27+
28+
context "when bundled speedscope does not exist" do
29+
before do
30+
allow(File).to receive(:exist?).with(described_class.bundled_index_html).and_return(false)
31+
end
32+
33+
it "opens with npx speedscope" do
34+
allow(described_class).to receive(:system).and_return(true)
35+
36+
described_class.open(profile_path)
37+
38+
expect(described_class).to have_received(:system).with("npx", "speedscope", profile_path)
39+
end
40+
end
41+
end
42+
43+
describe ".os_open_command" do
44+
it "returns a command and does not raise" do
45+
expect { described_class.send(:os_open_command) }.not_to raise_error
46+
expect(described_class.send(:os_open_command)).to match(/\A(start|open|xdg-open)\z/)
47+
end
48+
49+
context "when host_os is stubbed" do
50+
subject { described_class.send(:os_open_command) }
51+
52+
before do
53+
allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return(stubbed_os)
54+
end
55+
56+
context "on Windows" do
57+
let(:stubbed_os) { "mingw32" }
58+
59+
it { is_expected.to eq("start") }
60+
end
61+
62+
context "on MacOS" do
63+
let(:stubbed_os) { "darwin22.0" }
64+
65+
it { is_expected.to eq("open") }
66+
end
67+
68+
context "on Linux" do
69+
let(:stubbed_os) { "linux-gnu" }
70+
71+
it { is_expected.to eq("xdg-open") }
72+
end
73+
74+
context "on unsupported OS" do
75+
let(:stubbed_os) { "unknown-os" }
76+
77+
it "raises error" do
78+
expect { subject }.to raise_error(RuntimeError, /unknown-os/)
79+
end
80+
end
81+
end
82+
end
83+
end

0 commit comments

Comments
 (0)