Skip to content

Instantly share code, notes, and snippets.

@moyhdezzamawl
Created April 22, 2021 21:15
Show Gist options
  • Save moyhdezzamawl/e5cae644a8ec096b72e7a9dc5655f47b to your computer and use it in GitHub Desktop.
Save moyhdezzamawl/e5cae644a8ec096b72e7a9dc5655f47b to your computer and use it in GitHub Desktop.
enum FileCategory: String, Codable {
case dynamicLibrary = "Dynamic library"
case ui = "UI"
case codeSigning = "Code signing"
case resources = "Resources"
case localization = "Localization"
case others = "Others"
case code = "Code"
static func category(for fileName: String) -> FileCategory {
switch fileName.fileExtension {
case "dylib":
return .dynamicLibrary
case "nib":
return .ui
case "mobileprovision", "entitlements", "pem", "der":
return .codeSigning
case "gif", "ttf", "otf", "car", "png", "scnp", "sks", "json", "html", "m4a":
return .resources
case "stringsdict", "strings":
return .localization
case "xml", "pb", "lite", "gz", "dict", "mom", "txt", "sh", "plist":
return .others
default:
return .code
}
}
}
struct FileData: Codable {
var length: Int
var method: String
var size: Int
var name: String
var category: FileCategory
internal init(length: Int?, method: String, size: Int?, name: String) {
self.length = length ?? 0
self.method = method
self.size = size ?? 0
self.name = name
self.category = FileCategory.category(for: name)
}
static func factory(with str: String) -> FileData? {
let data = str
.trimmingCharacters(in: .whitespaces)
.components(separatedBy: " ")
.filter({ !$0.isEmpty })
guard data.count == 8 else { return nil }
let fileData = FileData(length: Int(data[0]),
method: data[1],
size: Int(data[2]),
name: data[7])
guard fileData.method == "Defl:N" else { return nil }
return fileData
}
}
guard let sizeAnalysis = try? SizeAnalysis() else {
print("somethig went wrong")
exit(EXIT_FAILURE)
}
print(sizeAnalysis.prettyPrinted())
class SizeAnalysis {
var sizeAnalysisData: SizeAnalysisData?
init() throws {
self.sizeAnalysisData = try self.readSizeAlaysisData()
}
func readSizeAlaysisData() throws -> SizeAnalysisData {
let environment: [String: String] = ProcessInfo.processInfo.environment
guard let ipaPath = environment["IPA_PATH"] else {
print("Please provide the export and ipa path")
exit(EXIT_FAILURE)
}
let zipPath = "ExampleApp.zip"
let sizeAnalysisDataPath = "size-analysis-data.txt"
shell("cp \(ipaPath) \(zipPath)")
shell("unzip -v \(zipPath) > \(sizeAnalysisDataPath)")
shell("rm \(zipPath)")
let data = try String(contentsOfFile: sizeAnalysisDataPath, encoding: .utf8)
let sizeAlaysisData = data.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: .newlines)
let filesData = sizeAlaysisData.compactMap(FileData.factory)
var report = SizeAnalysisData.factory(with: sizeAlaysisData.last ?? "") ?? SizeAnalysisData()
report.filesData = filesData
shell("rm \(sizeAnalysisDataPath)")
return report
}
@discardableResult
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
func prettyPrinted() -> String {
guard let data = self.sizeAnalysisData else {
return "The analysis has not be done yet"
}
var analysis = ""
analysis += "Size: \(data.sizeInMB.rounded(toPlaces: 2)) MB\n"
analysis += "Lenght: \(data.lengthInMB.rounded(toPlaces: 2)) MB\n"
analysis += "File count: \(data.fileCount)\n"
analysis += "Dependencies size: \(data.dependenceisSizeInMB.rounded(toPlaces: 2)) MB\n"
let categorySizes = data.fileCategorySizes ?? [:]
for category in categorySizes.keys {
let categorySize = categorySizes[category] ?? 0
let sizeInMb = Double(categorySize) / 1_000_000
analysis += "\(category.rawValue) size: \(sizeInMb.rounded(toPlaces: 2)) MB, \(Int(sizeInMb * 100 / data.sizeInMB))% of total app size\n"
}
return analysis
}
}
extension String {
var fileName: String {
URL(fileURLWithPath: self).deletingPathExtension().lastPathComponent
}
var fileExtension: String {
URL(fileURLWithPath: self).pathExtension
}
}
extension Double {
/// Rounds the double to decimal places value
func rounded(toPlaces places: Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
}
struct SizeAnalysisData {
var rowCount: Int = 0
var length: Int = 0
var size: Int = 0
var fileCount: Int = 0
var fileCategorySizes: [FileCategory: Int]?
var dependencySizes: [String: Int]?
var filesData: [FileData] = [] {
didSet {
self.rowCount = self.filesData.count
if self.fileCategorySizes == nil ||
self.dependencySizes == nil {
self.calculateSizes()
}
}
}
var sizeInMB: Double {
Double(size) / 1_000_000
}
var lengthInMB: Double {
Double(length) / 1_000_000
}
var fileCountForMetrics: Double {
Double(fileCount)
}
var dependenceisSizeInMB: Double {
let size: Double = self.dependencySizes?.reduce(0, { $0 + Double($1.value) }) ?? 0
return size / 1_000_000
}
static func factory(with str: String) -> SizeAnalysisData? {
let data = str
.trimmingCharacters(in: .whitespaces)
.components(separatedBy: " ")
.filter({ !$0.isEmpty })
guard data.count == 5 else { return nil }
return SizeAnalysisData(length: Int(data[0]) ?? 0,
size: Int(data[1]) ?? 0,
fileCount: Int(data[3]) ?? 0)
}
mutating func calculateSizes() {
var categorySizes = [FileCategory: Int]()
var dependencySizes = [String: Int]()
for fileData in filesData {
categorySizes.sum(fileData.size, forKey: fileData.category)
if fileData.name.contains(".bundle") {
addBundleSizeTo(dependencySizes: &dependencySizes,
fileData: fileData)
} else if fileData.name.contains("/Frameworks") {
addFrameworkSize(to: &dependencySizes, fileData: fileData)
}
}
if fileCategorySizes == nil {
self.fileCategorySizes = categorySizes
}
if self.dependencySizes == nil {
self.dependencySizes = dependencySizes
}
}
func addFrameworkSize(to dependencySizes: inout [String: Int], fileData: FileData) {
let components = fileData.name.components(separatedBy: "/")
guard components.count > 4 else { return }
let frameworkName = components[3]
dependencySizes.sum(fileData.size, forKey: frameworkName)
}
func addBundleSizeTo(dependencySizes: inout [String: Int], fileData: FileData) {
let components = fileData.name.components(separatedBy: "/")
guard components.count > 3 else { return }
let bundleName = components[2]
dependencySizes.sum(fileData.size, forKey: bundleName)
}
}
extension Dictionary where Key: Hashable, Value == Int {
mutating func sum(_ value: Value, forKey key: Key) {
let dependencySum = (self[key] ?? 0) + value
self.updateValue(dependencySum, forKey: key)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment