Skip to content

Instantly share code, notes, and snippets.

@edwardean
Created February 3, 2018 03:11
Show Gist options
  • Save edwardean/4409d5725be26c7c2b810141ad7cdad9 to your computer and use it in GitHub Desktop.
Save edwardean/4409d5725be26c7c2b810141ad7cdad9 to your computer and use it in GitHub Desktop.
解析LinkMap文件
//
// 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