Skip to content

Instantly share code, notes, and snippets.

@crosebrugh
Created May 1, 2014 01:33
Show Gist options
  • Save crosebrugh/0d384154b6ba9859881a to your computer and use it in GitHub Desktop.
Save crosebrugh/0d384154b6ba9859881a to your computer and use it in GitHub Desktop.
Analyze a .map file and output sizes of each pod, gem, the app itself, RubyMotion, iOS, as well the specific symbol sizes largest to smallest
#!/usr/bin/env ruby
# analyze a .map file and output sizes of each pod, gem, the app itself, RubyMotion, iOS
# also output functions (as per the -n option) sorted by size (high to low)
#
# change *_PREFIX constants and/or path_regexes to fit your specifics
#
# requires that RubyMotion is patched in order to generate the mapfile:
# /Library/RubyMotion2.26/lib/motion/project/builder.rb
#
# 305a306,308
# > mapfile = config.app_bundle(platform).sub(/[^.]+\z/,'map')
# > App.info 'Map', mapfile
# > linker_option << " -Wl,-map,#{mapfile.gsub(/\s/,'\ ')}"
require 'rubygems'
require 'optparse'
require 'fileutils'
require 'ruby-debug'
DEFAULT_MAPFILE="./build/iPhoneOS-7.1-Development/ulutu POS.map"
# can set this to anything. these prefix each function line from the mapfile
APP_PREFIX='A'
GEMS_PREFIX='G'
BUNDLER_GEMS_PREFIX='g'
BUILD_PREFIX='B'
RM_PREFIX='R'
XCODE_PREFIX='X'
def main
num_output_lines = 50 # default
optparse = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options] [mapfile (defaults to #{DEFAULT_MAPFILE})]"
opts.on("-n [INTEGER]", OptionParser::DecimalInteger,
"Output top 'n' lines (default #{num_output_lines}, blank means all)") do |n|
num_output_lines = n
end
opts.on( '-h', '--help', 'Display this screen') do
puts opts
exit
end
end
optparse.parse!
mapfile = ARGV[0] || DEFAULT_MAPFILE
# make sure that when the match is good $1 returns the actual match (hence the parens surrounding the path)
# these are checked against the Symbols section of the mapfile in order
path_regexes = [
[ Regexp.new("(/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk)"), XCODE_PREFIX ],
[ Regexp.new("(#{`pwd`.strip})"), APP_PREFIX ],
[ Regexp.new("(/lib/ruby/gems/1.9.1/gems)"), GEMS_PREFIX ],
[ Regexp.new("(/lib/ruby/gems/1.9.1/bundler/gems)"), BUNDLER_GEMS_PREFIX ],
[ Regexp.new("(#{File.dirname(mapfile)})"), BUILD_PREFIX ],
[ Regexp.new("(/Library/RubyMotion[^\/]*)"), RM_PREFIX ],
]
puts "NOTE: Analyzing: #{mapfile}"
puts
puts "NOTE: Prefixes"
path_regexes.each do |re, prefix|
puts " '#{prefix}' = #{re.to_s}"
end
linenum = 0
files = {} # idx => [ prefix, file ]
line_warns = { } # linenum => [ message, line ]
line_sizes = [] # [ size, func, prefix, file ]
segment_sizes = Hash.new(0)
File.open(mapfile,'r') do |fd|
while (line=fd.gets)
linenum += 1
#puts line
begin
if (line =~ /\A\[\s*(\d+)\]\s+(.*)\Z/)
idx = $1
file = $2
file_prefix = nil
path_regexes.each do |re, prefix|
if (s_idx=(file =~ re))
match_len = $1.length
if ((prefix == RM_PREFIX) && (file =~ /\(([^\)]*)\)/))
file = $1
else
file = file[(s_idx+match_len)..-1]
end
file_prefix = prefix
break
end
end
files[idx] = [ file_prefix, file || '' ]
#puts "#{linenum} >> '#{idx}' '#{file}'"
elsif (line =~ /\A0x[0-9A-F]+\s+0x([0-9A-F]+)\s+__([A-Z]+)\s+(.*)\Z/)
section = $3
next if (section == '__text')
size = hex_to_i($1)
segment = "Segment: #{$2}"
segment_sizes[segment] += size
elsif (line =~ /\A0x[0-9A-F]+\s+0x([0-9A-F]+)\s+\[\s*(\d+)\]\s+(.*)\Z/)
size = hex_to_i($1)
idx = $2
func = $3
file = files[idx]
line_sizes << [ size, func, file[0], file[1] ]
#puts "#{linenum} << #{idx} #{size} #{func}"
end
rescue ArgumentError => e
line_warns[linenum] = [ e.message, line ]
end
end
end
line_sizes = line_sizes.sort { |a,b| b[0] <=> a[0] }
# gather into pods/gems/app/etc...
total_size = 0
pod_sizes = Hash.new(0) # name => size
gem_sizes = Hash.new(0) # name => size
unknown_sizes = Hash.new(0) # name => size
app_size = 0
rm_size = 0
build_size = 0
line_sizes.each.with_index do |(size, func, prefix, file), idx|
case prefix
when APP_PREFIX
case file
when /\A\/vendor\/git\/([^\/]*)/
gem_sizes[$1] += size
when /\A\/pods\/([^\/]*)/
pod_sizes[$1] += size
when /\A\/vendor\/Pods\/([^\/]*)/
pod_sizes[$1] += size
when /\A\/app/
app_size += size
end
when GEMS_PREFIX
case file
when /\A\/([^\/]*)/
gem_sizes[$1] += size
end
when BUNDLER_GEMS_PREFIX
case file
when /\A\/([^\/]*)/
gem_sizes[$1] += size
end
when BUILD_PREFIX
build_size += size
when RM_PREFIX
rm_size += size
when XCODE_PREFIX
build_size += size
end || (unknown_sizes[file] += size)
total_size += size
end
sizes = [
[ rm_size, 'RubyMotion' ],
[ app_size, 'app' ],
[ build_size, 'iOS' ]
]
gem_sizes.sort_by { |k, v| v }.reverse.each do |k, v|
sizes << [ v, "Gem #{k}" ]
end
pod_sizes.sort_by { |k, v| v }.reverse.each do |k, v|
sizes << [ v, "Pod #{k}" ]
end
unknown_sizes.sort_by { |k, v| v }.reverse.each do |k, v|
sizes << [ v, k ]
end
segment_sizes.sort_by { |k, v| v }.reverse.each do |k, v|
total_size += v
sizes << [ v, k ]
end
sizes.sort! { |a,b| b[0] <=> a[0] }
puts
puts "NOTE: sizes (MB)"
puts " #{i_to_mb(total_size)} - TOTAL"
sizes.each do |v, str|
v = i_to_mb(v)
puts " #{v} - #{str}" if (v != "0.00")
end
#
puts
line_warns.each do |k,v|
puts "WARN: #{v[0]}"
puts " line #{k}: #{v[1]}"
end
#
unless (num_output_lines && (num_output_lines == 0))
#
puts
puts "NOTE: All functions largest to smallest: 'size (in bytes)' 'prefix' 'file' 'function'"
line_sizes.each.with_index do |(size, func, prefix, file), idx|
size = sprintf("%9d",size)
puts("#{size} #{prefix} #{file} #{func}") if (!num_output_lines || (idx < num_output_lines))
end
end
''
end
def hex_to_i(hex)
[hex].pack('H*').unpack('L>')[0]
end
MB=1024.0*1024.0
def i_to_mb(v)
r = sprintf("%8.2f",(v || 0).to_f/MB)
end
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment