Skip to content

Commit 0b9043c

Browse files
committed
Add scripts for profiling in Instruments
1 parent d4dfa49 commit 0b9043c

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed

make_signed_ruby.rb

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# This script copies and signs the current `ruby` executable,
2+
# so that it can be run in a debugger or profiler, like Xcode's
3+
# Instruments.
4+
#
5+
# The output folder is configured by `destination_folder` below,
6+
# but it's just the current dir by default.
7+
# This script will create a new `Ruby.app` bundle in that dest.
8+
#
9+
# Before running this script:
10+
#
11+
# 1. Create a signing certificate for yourself.
12+
#
13+
# Follow the instructions under the heading "To obtain a self-signed certificate using Certificate Assistant"
14+
# https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html
15+
#
16+
# The defaults worked for me, so I didn't need to check "Let me override defaults".
17+
#
18+
# Once done, it should show up when you run `security find-identity`
19+
#
20+
# 2. Set `$signing_cert_name` to the name of your new certificate
21+
#
22+
# 3. Run `sudo DevToolsSecurity -enable` (Optional)
23+
# This will make it so you don't get prompted for a password every time you run a debugger/profiler.
24+
#
25+
# After running this script, you can either use the `dest/Ruby.app/Contents/macOS/ruby` binary from there,
26+
# or copy it to another place of your choosing.
27+
28+
require "rbconfig"
29+
require "pathname"
30+
require "tempfile"
31+
32+
# Inputs
33+
$signing_cert_name = "'Alexander Momchilov (Shopify)'"
34+
destination_folder = Pathname.pwd
35+
36+
app_bundle = create_bundle_skeleton(at: destination_folder)
37+
38+
copy_ruby_executable(into: app_bundle)
39+
40+
sign_bundle(app_bundle)
41+
42+
if verify_entitlements(app_bundle)
43+
puts "Successfully created a signed Ruby at: #{app_bundle}"
44+
exit(true)
45+
else
46+
puts "Something went wrong."
47+
exit(false)
48+
end
49+
50+
51+
52+
53+
54+
BEGIN { # Helper methods
55+
def create_bundle_skeleton(at:)
56+
destination = at
57+
58+
app_bundle = (destination / "Ruby.app").expand_path
59+
contents_folder = app_bundle / "Contents"
60+
61+
contents_folder.mkpath
62+
63+
info_plist_content = <<~INFO_PLIST
64+
<?xml version="1.0" encoding="UTF-8"?>
65+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
66+
<plist version="1.0">
67+
<dict>
68+
<key>CFBundleInfoDictionaryVersion</key>
69+
<string>6.0</string>
70+
<key>CFBundleExecutable</key>
71+
<string>ruby</string>
72+
<key>CFBundleIdentifier</key>
73+
<string>com.shopify.amomchilov.Ruby</string>
74+
<key>CFBundleName</key>
75+
<string>Ruby</string>
76+
<key>CFBundleDisplayName</key>
77+
<string>Ruby</string>
78+
<key>CFBundleShortVersionString</key>
79+
<string>#{RUBY_VERSION}</string>
80+
<key>CFBundleVersion</key>
81+
<string>#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}</string>
82+
<key>NSHumanReadableCopyright</key>
83+
<string>#{RUBY_COPYRIGHT.delete_prefix("ruby - ")}</string>
84+
</dict>
85+
</plist>
86+
INFO_PLIST
87+
88+
(contents_folder / "Info.plist").write(info_plist_content)
89+
90+
app_bundle
91+
end
92+
93+
def copy_ruby_executable(into:)
94+
app_bundle = into
95+
destination = (app_bundle / "Contents/MacOS")
96+
97+
begin
98+
destination.mkpath
99+
rescue Errno::EEXIST
100+
# Folder already exists. No problem.
101+
end
102+
103+
original_ruby = Pathname.new(RbConfig.ruby)
104+
puts "Copying Ruby #{RUBY_VERSION} from #{original_ruby}"
105+
FileUtils.cp(original_ruby, destination)
106+
destination
107+
end
108+
109+
def sign_bundle(bundle_path)
110+
entitlements_plist = <<~PLIST
111+
<?xml version="1.0" encoding="UTF-8"?>
112+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
113+
<plist version="1.0">
114+
<dict>
115+
<key>com.apple.security.get-task-allow</key>
116+
<true/>
117+
118+
<!-- https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.security.cs.disable-library-validation -->
119+
<key>com.apple.security.cs.disable-library-validation</key>
120+
<true/>
121+
</dict>
122+
</plist>
123+
PLIST
124+
125+
Tempfile.create("entitlements.plist") do |entitlements_file|
126+
entitlements_path = entitlements_file.path
127+
128+
args = [
129+
"/usr/bin/codesign",
130+
"--force", # Replace the existing signiture, if any
131+
"--sign", $signing_cert_name,
132+
"-o", "runtime",
133+
"--entitlements", entitlements_file.path,
134+
"--timestamp\\=none",
135+
"--generate-entitlement-der", # necessary?
136+
"#{bundle_path}"
137+
]
138+
139+
entitlements_file.puts(entitlements_plist)
140+
entitlements_file.flush
141+
142+
puts "\nSigning #{bundle_path}."
143+
puts "----- codesign output:"
144+
system(args.join(" "), exception: true)
145+
puts "-----\n\n"
146+
end
147+
148+
nil
149+
end
150+
151+
def verify_entitlements(bundle_path)
152+
puts "Verifying the code signature..."
153+
entitlements_xml = `codesign --display --entitlements - --xml #{bundle_path} 2>/dev/null`
154+
155+
disable_lib_validation = "com.apple.security.cs.disable-library-validation"
156+
allow_debugging = "com.apple.security.get-task-allow"
157+
158+
issues = []
159+
160+
# Doing dumb string matching, so we don't need to pull in an XML/Plist parser.
161+
if entitlements_xml.include?(disable_lib_validation)
162+
if entitlements_xml.include?("<key>#{disable_lib_validation}</key><true/>")
163+
puts "\t- ✅ Entitlement #{disable_lib_validation.inspect} was set correctly"
164+
else
165+
issues << "\t- ❌ #{disable_lib_validation.inspect} was not `true` in the bundle's entitlements."
166+
end
167+
else
168+
issues << "\t- ❌ #{disable_lib_validation.inspect} was missing from the bundle's entitlements."
169+
end
170+
171+
if entitlements_xml.include?(allow_debugging)
172+
if entitlements_xml.include?("<key>#{allow_debugging}</key><true/>")
173+
puts "\t- ✅ Entitlement #{allow_debugging.inspect} was set correctly"
174+
else
175+
issues << "\t- ❌ #{allow_debugging.inspect} was not `true` in the bundle's entitlements."
176+
end
177+
else
178+
issues << "\t- ❌ #{allow_debugging.inspect} was missing from the bundle's entitlements."
179+
end
180+
181+
if issues.any?
182+
puts "There were issues with the code-signing:"
183+
puts issues
184+
puts
185+
return false
186+
end
187+
188+
puts
189+
190+
true
191+
end
192+
}

trace_rbs.sh

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Profile the RBS tests in Instruments.
2+
#
3+
# Based on https://www.jviotti.com/2024/01/29/using-xcode-instruments-for-cpp-cpu-profiling.html
4+
#
5+
# The usual Instruments templates are:
6+
# --template 'Allocations'
7+
# --template 'CPU Profiler'
8+
#
9+
# ...but I made myself a custom template called "Ruby" with these instruments:
10+
# - Allocations
11+
# - Points of Integer
12+
# - os_signpost
13+
# - stdout/stderr
14+
# - Time Profiler
15+
# - Sampler
16+
#
17+
# The "Leaks" instrument doesn't work, and IDK why... some issue with libmalloc.
18+
19+
cd /Users/alex/src/github.com/Shopify/rbs
20+
21+
source /opt/dev/sh/chruby/chruby.sh
22+
chruby 3.3.4
23+
24+
TRACE_FILE="$(mktemp -t trace_XXXXXX).trace"
25+
26+
echo "Will save trace file to $TRACE_FILE"
27+
28+
xcrun xctrace record \
29+
--template 'Ruby' \
30+
--no-prompt \
31+
--output "$TRACE_FILE" \
32+
--target-stdout - \
33+
--launch \
34+
-- \
35+
/Users/alex/Downloads/ruby \
36+
-w -Ilib -Itest \
37+
/Users/alex/.gem/ruby/3.3.0/gems/rake-13.2.1/lib/rake/rake_test_loader.rb \
38+
"test/rbs/signature_parsing_test.rb" \
39+
40+
# "test/rbs/ancestor_builder_test.rb" \
41+
# "test/rbs/ancestor_graph_test.rb" \
42+
# "test/rbs/annotate/annotations_test.rb" \
43+
# "test/rbs/annotate/rdoc_annotator_test.rb" \
44+
# "test/rbs/annotate/rdoc_source_test.rb" \
45+
# "test/rbs/ast/type_param_test.rb" \
46+
# "test/rbs/ast/visitor_test.rb" \
47+
# "test/rbs/buffer_test.rb" \
48+
# "test/rbs/cli_test.rb" \
49+
# "test/rbs/collection/cleaner_test.rb" \
50+
# "test/rbs/collection/config_test.rb" \
51+
# "test/rbs/collection/installer_test.rb" \
52+
# "test/rbs/collection/sources/git_test.rb" \
53+
# "test/rbs/collection/sources/local_test.rb" \
54+
# "test/rbs/collection/sources/stdlib_test.rb" \
55+
# "test/rbs/definition_builder_test.rb" \
56+
# "test/rbs/diff_test.rb" \
57+
# "test/rbs/environment_loader_test.rb" \
58+
# "test/rbs/environment_test.rb" \
59+
# "test/rbs/environment_walker_test.rb" \
60+
# "test/rbs/errors_test.rb" \
61+
# "test/rbs/factory_test.rb" \
62+
# "test/rbs/file_finder_test.rb" \
63+
# "test/rbs/location_test.rb" \
64+
# "test/rbs/locator_test.rb" \
65+
# "test/rbs/method_builder_test.rb" \
66+
# "test/rbs/method_type_parsing_test.rb" \
67+
# "test/rbs/node_usage_test.rb" \
68+
# "test/rbs/parser_test.rb" \
69+
# "test/rbs/rb_prototype_test.rb" \
70+
# "test/rbs/rbi_prototype_test.rb" \
71+
# "test/rbs/rdoc/rbs_parser_test.rb" \
72+
# "test/rbs/repository_test.rb" \
73+
# "test/rbs/resolver/constant_resolver_test.rb" \
74+
# "test/rbs/resolver/type_name_resolver_test.rb" \
75+
# "test/rbs/runtime_prototype_test.rb" \
76+
# "test/rbs/schema_test.rb" \
77+
# "test/rbs/sorter_test.rb" \
78+
# "test/rbs/subtractor_test.rb" \
79+
# "test/rbs/test/hook_test.rb" \
80+
# "test/rbs/test/runtime_test_test.rb" \
81+
# "test/rbs/test/setup_helper_test.rb" \
82+
# "test/rbs/test/tester_test.rb" \
83+
# "test/rbs/test/type_check_test.rb" \
84+
# "test/rbs/type_alias_dependency_test.rb" \
85+
# "test/rbs/type_alias_regulartiry_test.rb" \
86+
# "test/rbs/type_parsing_test.rb" \
87+
# "test/rbs/types_test.rb" \
88+
# "test/rbs/use_map_test.rb" \
89+
# "test/rbs/variance_calculator_test.rb" \
90+
# "test/rbs/vendorer_test.rb" \
91+
# "test/rbs/writer_test.rb" \
92+
# "test/validator_test.rb"
93+
94+
echo "xtrace exited with code $?"
95+
# 2 => fail
96+
# 0 => success
97+
# 54 => also success?
98+
99+
open "$TRACE_FILE"

0 commit comments

Comments
 (0)