Created
April 22, 2021 21:15
-
-
Save moyhdezzamawl/e5cae644a8ec096b72e7a9dc5655f47b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
guard let sizeAnalysis = try? SizeAnalysis() else { | |
print("somethig went wrong") | |
exit(EXIT_FAILURE) | |
} | |
print(sizeAnalysis.prettyPrinted()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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