Created
February 3, 2018 03:11
-
-
Save edwardean/4409d5725be26c7c2b810141ad7cdad9 to your computer and use it in GitHub Desktop.
解析LinkMap文件
This file contains hidden or 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
// | |
// LinkMapParser.swift | |
// | |
// Created by lihang on 02/02/2018. | |
// | |
import Foundation | |
public typealias SortDescriptor<Value> = (Value, Value) -> Bool | |
public func sortDescriptor<Value, Key>(key: @escaping (Value) -> Key, by areInIncreasingOrder: @escaping (Key, Key) -> Bool) -> SortDescriptor<Value> where Key: Comparable { | |
return { | |
areInIncreasingOrder(key($0), key($1)) | |
} | |
} | |
public func sortDescriptor<Value, Key>(key: @escaping (Value) -> Key, ascending: Bool = true, by comparator: @escaping (Key) -> (Key) -> ComparisonResult) -> SortDescriptor<Value> { | |
return { lhs, rhs in | |
let order: ComparisonResult = ascending ? .orderedAscending : .orderedDescending | |
return comparator(key(lhs))(key(rhs)) == order | |
} | |
} | |
public func lift<T>(_ compare: @escaping (T) -> (T) -> ComparisonResult) -> (T?) -> (T?) -> ComparisonResult { | |
return { lhs in { rhs in | |
switch (lhs, rhs) { | |
case (nil, nil): return .orderedSame | |
case (nil, _): return .orderedAscending | |
case (_, nil): return .orderedDescending | |
case let (l?, r?): return compare(l)(r) | |
} | |
} } | |
} | |
public func combine<Value>(sortDescriptors: [SortDescriptor<Value>]) -> SortDescriptor<Value> { | |
return { lhs, rhs in | |
for areInIncreasingOrder in sortDescriptors { | |
if areInIncreasingOrder(lhs, rhs) { | |
return true | |
} | |
if areInIncreasingOrder(rhs, lhs) { | |
return false | |
} | |
} | |
return false | |
} | |
} | |
struct linkMapSymbol { | |
var objectFile: String? | |
var libName: String? | |
var size: UInt = 0 | |
} | |
typealias SizeUnitContext = (unit: Double, descriptor: String) | |
let SizeUnitContexts: [SizeUnitContext] = { | |
let contexts = [SizeUnitContext(1.0, "B"), | |
SizeUnitContext(1024.0, "KB"), | |
SizeUnitContext(1024*1024.0, "MB"), | |
SizeUnitContext(1024*1024*1024.0, "GB")] | |
let sortByUnit: SortDescriptor<SizeUnitContext> = sortDescriptor(key: { $0.unit }, by: <) | |
return contexts.sorted(by: sortByUnit) | |
}() | |
let numberFormatter: NumberFormatter = { | |
let formatter = NumberFormatter() | |
formatter.maximumFractionDigits = 4 | |
return formatter | |
}() | |
func formatSizeNumber(_ size: Double, descriptor: String = "") -> String { | |
guard let pureString = numberFormatter.string(from: NSNumber(value: size)) else { | |
return "\(size)" + descriptor | |
} | |
return pureString + descriptor | |
} | |
func formatSize(_ size: Double) -> String { | |
var prevContext = SizeUnitContexts[0] | |
for (index, sizeContext) in SizeUnitContexts.enumerated() { | |
if index == 0 { | |
if size < sizeContext.unit { | |
return formatSizeNumber(size, descriptor: sizeContext.descriptor) | |
} | |
} else { | |
if size >= prevContext.unit && size < sizeContext.unit { | |
return formatSizeNumber(size / prevContext.unit, descriptor: prevContext.descriptor) | |
} | |
} | |
prevContext = sizeContext | |
} | |
return formatSizeNumber(size / prevContext.unit, descriptor: prevContext.descriptor) | |
} | |
private func parseFile(_ linkMapFileURL: URL) { | |
guard FileManager.default.fileExists(atPath: linkMapFileURL.path) else { | |
print("❗️ 未找到LinkMap文件") | |
return | |
} | |
guard linkMapFileURL.pathExtension == "txt" else { | |
print("❗️ \(linkMapFileURL) 不是txt文件") | |
return | |
} | |
var fileContent: String? | |
do { | |
fileContent = try NSString(contentsOf: linkMapFileURL, encoding: String.Encoding.macOSRoman.rawValue) as String | |
} catch { | |
print(error) | |
} | |
print("🔄 parseLinkMap: \(linkMapFileURL)...") | |
guard let content = fileContent else { return } | |
guard let _ = content.range(of: "# Path:"), let objsFileTagRange = content.range(of: "# Object files:") else { return } | |
let subObjsFileSymbolStr = content[objsFileTagRange.upperBound...] | |
guard let _ = subObjsFileSymbolStr.range(of: "# Symbols:") else { return } | |
let lines = content.split(separator: "\n").map ( String.init ) | |
var sizeMap: [String: linkMapSymbol] = [:] | |
var currentSectionName: String? | |
var currentLinkMapFilePath: String? | |
var currentArch: String? | |
var reachFiles = false | |
var reachSymbols = false | |
var reachSections = false | |
func startSection(_ sectionLine: String) { | |
guard let semicolonIndex = sectionLine.index(of: ":") else { return } | |
let sectionName = String(sectionLine[..<semicolonIndex]) | |
currentSectionName = sectionName | |
let last = trimmingLineWhitespace(String(sectionLine[semicolonIndex...])) | |
if sectionName.contains("Path") { | |
currentLinkMapFilePath = last | |
} else if sectionName.contains("Arch") { | |
currentArch = last | |
} | |
} | |
func parseSection(_ sectionLine: String) { | |
guard let currentSectionName = currentSectionName else { return } | |
if currentSectionName.contains("Object files") { | |
parseObjectLine(sectionLine) | |
} else if currentSectionName.contains("Symbols") { | |
parseSymbolLine(sectionLine) | |
} | |
} | |
func parseObjectLine(_ objectLine: String) { | |
/* | |
业务类: | |
[811] /Users/jenkins/jenkins-slave-home/workspace/COS-Moma/FastlaneAlpha/hyperlane_sandbox/build/Build/Intermediates.noindex/ArchiveIntermediates/MOMA/IntermediateBuildFilesPath/MOMA.build/DailyBuild-iphoneos/MOMA.build/Objects-normal/arm64/HistogramView.o | |
Pod类: | |
[813] /Users/jenkins/jenkins-slave-home/workspace/COS-Moma/FastlaneAlpha/hyperlane_sandbox/build/Build/Intermediates.noindex/ArchiveIntermediates/MOMA/BuildProductsPath/DailyBuild-iphoneos/AFNetworking/libAFNetworking.a(AFHTTPRequestOperation.o) | |
*/ | |
guard let leftQuareBracketIndex = objectLine.index(of: "["), let rightQuareBracketIndex = objectLine.index(of: "]") else { return } | |
//截取[811]中的811 | |
let objectIndex = trimmingLineWhitespace(String(objectLine[leftQuareBracketIndex..<rightQuareBracketIndex])) | |
let filePathText = trimmingLineWhitespace((objectLine[rightQuareBracketIndex...] as NSString).lastPathComponent) | |
if let leftParenthesisIndex = filePathText.index(of: "("), let rightParenthesisIndex = filePathText.index(of: ")") { | |
let objectFile = filePathText[filePathText.index(after: leftParenthesisIndex)..<rightParenthesisIndex] //AFHTTPRequestOperation.o | |
let libName = filePathText[..<leftParenthesisIndex] //libAFNetworking.a | |
sizeMap[objectIndex, default: linkMapSymbol()].objectFile = String(objectFile) | |
sizeMap[objectIndex, default: linkMapSymbol()].libName = String(libName) | |
} else { | |
if let objectsNormalRange = objectLine.range(of: "/Objects-normal") { | |
let libName = (objectLine[..<objectsNormalRange.lowerBound] as NSString).lastPathComponent | |
sizeMap[objectIndex, default: linkMapSymbol()].libName = libName //MOMA.build | |
sizeMap[objectIndex, default: linkMapSymbol()].objectFile = filePathText //HistogramView.o | |
} | |
} | |
} | |
func parseSymbolLine(_ symbolLine: String) { | |
/* | |
# Symbols: | |
# Address Size File Name | |
0x100006824 0x00000130 [ 2] +[DEFDealDishListItem replacedKeyFromPropertyName] | |
0x100006954 0x000000B0 [ 2] +[DEFDealDishListItem objectClassInArray] | |
0x100006A04 0x00000424 [ 2] -[DEFDealDishListItem copySelf] | |
0x100006E28 0x000006D8 [ 2] -[DEFDealDishListItem selectedDish] | |
*/ | |
let symbols = symbolLine.split(separator: "\t") | |
guard symbols.count == 3 else { return } | |
let fileKeyAndName = symbols[2] | |
guard let leftQuareBracketIndex = fileKeyAndName.index(of: "["), let rightQuareBracketIndex = fileKeyAndName.index(of: "]") else { return } | |
let objectIndex = trimmingLineWhitespace(String(fileKeyAndName[leftQuareBracketIndex..<rightQuareBracketIndex])) | |
let size = strtoul(symbols[1].cString(using: .utf8), nil, 16) | |
sizeMap[objectIndex]?.size += size | |
} | |
lines.forEach { line in | |
if line.hasPrefix("#") { | |
startSection(line) | |
} else { | |
parseSection(line) | |
} | |
} | |
let symbolNameCompare = lift(String.localizedStandardCompare) | |
let libNameCompare = lift(String.localizedStandardCompare) | |
let sortBySymbolName: SortDescriptor<linkMapSymbol> = sortDescriptor(key: { $0.objectFile }, by: symbolNameCompare) | |
let sortByLibName: SortDescriptor<linkMapSymbol> = sortDescriptor(key: { $0.libName }, by: libNameCompare) | |
let sortBySymbolSize: SortDescriptor<linkMapSymbol> = sortDescriptor(key: { $0.size }, by: >) | |
let combined: SortDescriptor<linkMapSymbol> = combine(sortDescriptors: [sortBySymbolSize, sortBySymbolName, sortByLibName]) | |
let sortedSizeMap = Array(sizeMap.values).sorted(by: combined) | |
guard !sortedSizeMap.isEmpty else { | |
print("❗️ linkMap解析数据为空") | |
return | |
} | |
var libMap: [String: Double] = [:] | |
var html = """ | |
<html> \ | |
<head>\ | |
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\ | |
<title>LinkMap解析</title>\ | |
<style type=\"text/css\">\ | |
table {\ | |
width: 100%;\ | |
border-right:1px solid #490;\ | |
border-bottom:1px solid #490;\ | |
}\ | |
table td{\ | |
border-left:1px solid #490;\ | |
border-top:1px solid #490;\ | |
}\ | |
</style>\ | |
</head>\ | |
<body> | |
""" | |
html += "<div>" | |
if let currentLinkMapFilePath = currentLinkMapFilePath { | |
html += "<h1>File\(currentLinkMapFilePath)</h1>" | |
} | |
if let currentArch = currentArch { | |
html += "<h2>Arch\(currentArch)</h2>" | |
} | |
html += "</div>" | |
var objectDiv = """ | |
<div>\ | |
<h2>Object Files:</h2> \ | |
<table>\ | |
<tr><td>File Name</td><td>Size</td><td>Static Library Or Framework</td></tr> | |
""" | |
var totalObjectSize: Double = 0 | |
sortedSizeMap.forEach { mapSymbol in | |
if let filePath = mapSymbol.objectFile, let libName = mapSymbol.libName { | |
objectDiv += "<tr><td>\(filePath)</td><td>\(formatSize(Double(mapSymbol.size)))</td><td>\(libName)</td></tr>" | |
libMap[libName, default: 0] += Double(mapSymbol.size) | |
totalObjectSize += Double(mapSymbol.size) | |
} | |
} | |
objectDiv += "<tr><td>总计</td><td>\(formatSize(totalObjectSize))</td><td></td></tr>" | |
var libDiv = """ | |
<div>\ | |
<h2>Library Or Framework Link Size:</h2> \ | |
<table>\ | |
<tr><td>Static Library Or Framework</td><td>Size</td></tr> | |
""" | |
var totalLibSize: Double = 0 | |
libMap.sorted(by: { $0.1 > $1.1 }).forEach { libName, size in | |
libDiv += "<tr><td>\(libName)</td><td>\(formatSize(size))</td></tr>" | |
totalLibSize += size | |
} | |
libDiv += "<tr><td>总计</td><td>\(formatSize(totalLibSize))</td></tr>" | |
html += libDiv | |
html += "</table></div>" | |
html += objectDiv | |
html += "</table></div>" | |
html += """ | |
</body>\ | |
</html> | |
""" | |
let resultFileOutputURL = linkMapFileURL.deletingPathExtension().appendingPathExtension("html") | |
do { | |
try html.write(to: resultFileOutputURL, atomically: true, encoding: .utf8) | |
} catch { | |
print(error) | |
} | |
print("✅ linkMap解析结果已写入到\(resultFileOutputURL)中") | |
} | |
public func ParseLinkMapFile(_ sandBoxPath: String) { | |
let fileManager = FileManager.default | |
guard fileManager.fileExists(atPath: sandBoxPath) else { | |
print("❗️ can not find sandBoxPath") | |
return | |
} | |
let sandBoxPathURL = URL(fileURLWithPath: sandBoxPath) | |
var fileURL: URL? | |
var linkMapFilePaths: [URL] = [] | |
do { | |
let contents = try fileManager.contentsOfDirectory(at: sandBoxPathURL, includingPropertiesForKeys: nil) | |
let linkMapFiles = contents.lazy.filter { | |
if $0.absoluteString.range(of: "-LinkMap-") != nil { | |
return true | |
} | |
return false | |
} | |
linkMapFilePaths.append(contentsOf: linkMapFiles) | |
if let linkMapFilePath = linkMapFiles.first { | |
fileURL = linkMapFilePath | |
} | |
} catch { | |
print(error) | |
} | |
let semaphore = DispatchSemaphore(value: 0) | |
let queue = DispatchQueue.global() | |
let workItem = DispatchWorkItem(qos: .utility) { | |
defer { | |
semaphore.signal() | |
} | |
for linkMapFile in linkMapFilePaths { | |
parseFile(linkMapFile) | |
} | |
} | |
queue.async(execute: workItem) | |
semaphore.wait() | |
} | |
func trimmingLineWhitespace(_ line: String) -> String { | |
return line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) | |
} | |
let arguments = CommandLine.arguments | |
print("⚠️ arguments: \(arguments)") | |
if arguments.count >= 2 { | |
let sandboxPath = arguments[1] | |
ParseLinkMapFile(sandboxPath) | |
} else { | |
print("❗️ 缺少sandbox输入参数!") | |
print("======用法======") | |
print("swift LinkMapParser.swift LinkMap文件所在目录") | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment