轻量级APP启动信息构建方案
背景
在头条的启动框架下,启动任务已经划分的较为明确,而启动时序是启动任务中的关键信息。目前我们获取这些信息的主要手段是看systrace,但直接读systrace存在一些问题:
- systrace在release下一些信息不全,例如IO线程信息,而启动优化的主要评估场景是release
- systrace信息相对较重,可阅读性差,同时对启动任务的阅读的干扰性大
在上述问题的影响下,会增加开发人员排查、验证启动任务问题,以及优化启动任务的难度。
因此本文考虑设计一个轻量级的信息描述、收集与信息重建方案,灵活适应release模式与debug模式,同时增加可阅读性,降低开发人员排查问题的成本。
1 方案设计
轻量级启动信息构建方案主要由三部分组成:
- 启动信息构建:负责提炼关键信息做成新数据结构
- 启动信息收集:负责收集、输出各个任务的信息到重建模块
- 启动信息重建:负责信息构建、输出可视化图形
2 具体模块实现
2.1 启动信息构建
data class InitDataStruct(
var startTime: Long = 0,
var duration: Long = 0,
var currentProcess: String = "",
var taskName: String = ""
)
复制代码
关键的启动信息主要有这么几个维度:
- 启动时间(归一化)
- 启动耗时
- 启动线程
- 启动名称
而并不关心,即需要剔除掉的任务:
- 非启动任务信息(这并不是说它不重要,只是在启动框架这一环它并不是高优)
- 启动任务stack
Format形如
{"task_name":"class com.xxx.xxxTask","start_time":5,"duration":9,"current_process":"AA xxxThread#4"}
复制代码
2.2 启动信息收集
由于没接入公司平台(太小),因此考虑就以log的方式输出结果。
大概是希望实现下面的功能,但一个一个加就有点复制粘贴有点太low了
调研了一下有一种AspectJ的做法,可以利用
@PointCut("execution(* com.xxx.xxx.xxxTask.run(*))")
在task周围埋下切入点
利用@Before
、@After
注入切入代码即可。
2.3 启动信息收集与绘制
由于目前是依赖人工进行启动分析,因此我们收集启动信息的手段依赖于Console打印的日志,形如
{"task_name":"class com.xxx.Task","start_time":0,"duration":2,"current_process":"main"}
复制代码
这里我们直接写个读取工具给他转义一下,让他变成具有可读性的数据结构
# 在Client中以json保存下来的
def toInitInfo(json):
return InitInfo(json["start_time"], json["duration"], json["current_process"], str(json["task_name"]).split('.')[-1])
class InitInfo:
#startTime和duration均做了归一化
def __init__(self, startTime, duration, currentProcessName, taskName):
self.startTime = startTime
self.taskName = taskName
self.duration = duration
self.currentProcessName = currentProcessName
def printitself(self):
print("task_name : " + self.taskName)
print("\tstartTime : " + str(self.startTime))
print("\tduration : " + str(self.duration))
print("\tcurrentProcessName : " + self.currentProcessName)
# 获取task时长
def getNameCombineDuration(self):
return self.taskName + " " + str(self.duration)
# 获取当前打印的最大长度
def getConstructLen(self):
return len(self.getNameCombineDuration()) + 2
def generateFormatStr(self, perTime, perBlank):
totalLen = max(3, int(1.0 * perBlank * max(1, self.duration) / perTime))
cntLen = max(0, totalLen - self.getConstructLen())
strr = "|" + (cntLen / 2 + cntLen % 2) * "-" + self.getNameCombineDuration()[0:min(totalLen - 2, len(self.getNameCombineDuration()))]+ cntLen / 2 * "-" + "|"
return strr
def generateBlank(self, timeNow, perTime, perBlank):
strr = max(0, int((self.startTime - timeNow) / perTime) * perBlank) * " "
return strr
复制代码
并将所有task插入到list中,以完成时间作为sort Function
def sortByEnd(initInfo1, initInfo2):
return (initInfo1.startTime + initInfo1.duration) <= (initInfo2.startTime + initInfo2.duration)
def dealWithList():
for item in line_jsons:
if(taskMap.has_key(item.currentProcessName)):
taskMap[item.currentProcessName].append(item)
else:
taskMap[item.currentProcessName] = []
taskMap[item.currentProcessName].append(item)
复制代码
现在到了问题的核心,我们该采用什么规则把绘图绘制出来,这取决于我们需要得到的信息有哪些:
- 第一种:分析启动任务耗时,可采用类似systrace,横轴为固定的单位时间长度,纵轴是currentProcess
def drawMp():
duraLen = 0
maxLen = 0
# 10ms间隔
currentPerTime = 10
endFile = open("timeline.txt","w")
# 先保证起始坐标轴一致
for key in taskMap.keys():
maxLen = max(maxLen, len(key))
# 计算最长字符串
for item in line_jsons:
duraLen = max(duraLen, item.getConstructLen())
# 画个坐标轴
xplot = maxLen * " " + " :"
for index in range(0, (line_jsons[-1].startTime + line_jsons[-1].duration) / currentPerTime):
cntLen = duraLen - 2 - len(str(index * currentPerTime))
xplot += "|" + (cntLen / 2 + cntLen % 2) * "-" + str(index * currentPerTime) + cntLen / 2 * "-" + "|"
endFile.write(xplot + "\n")
# 画图
for key in taskMap.keys():
strr = key + (maxLen - len(key)) * " " + " :"
timeNow = 0
for item in taskMap[key]:
item.printitself()
strr += item.generateBlank(timeNow, perTime = currentPerTime, perBlank = duraLen)
strr += item.generateFormatStr(10, duraLen)
timeNow = item.startTime + item.duration
strr += "\n"
endFile.write(strr)
endFile.close()
复制代码
- 第二种:分析启动任务排布的合理性,即是否存在长尾型的启动路径,这里考虑横轴为离散化后的启动任务时间,纵轴为currentProcess
## 第二种画图法:离散
# 离散点阵图
duraCordi = []
def drawMp2():
# 离散单位区间长度
duraLen = 0
def addBlank(st, ed):
return (ed - st) * duraLen * " "
def formatString(st, ed, taskName, duraLen):
strr = "|"
leftBlank = (ed - st) * duraLen - 2 - len(taskName)
strr += (leftBlank / 2 + leftBlank % 2) * "-"
strr += taskName
strr += leftBlank / 2 * "-" + "|"
return strr
# 先离散
# 最短是 -> |maxLen(xxxTask)|
dura = []
filee = open("timeline2.txt","w")
for item in line_jsons:
duraLen = max(duraLen, len(item.getNameCombineDuration()) + 2)
dura.append(item.startTime)
dura.append(item.startTime + item.duration)
duraCordi = list(set(dura))
duraCordi.sort()
print(duraCordi)
#再遍历塞值进去
maxLen = 0
for key in taskMap.keys():
maxLen = max(maxLen, len(key))
for key in taskMap.keys():
currentIndex = 0
strr = key + (maxLen - len(key)) * " " + " :"
for item in taskMap[key]:
stIndex = bisect.bisect_left(duraCordi, item.startTime)
edIndex = bisect.bisect_left(duraCordi, item.startTime + max(item.duration, 1))
strr += addBlank(currentIndex, stIndex)
strr += formatString(stIndex, edIndex, item.getNameCombineDuration(), duraLen = duraLen)
currentIndex = edIndex
strr += "\n"
filee.write(strr)
filee.close()
复制代码
3 效果对比
- 第一种启动耗时为单位的
- 第二种启动时间离散化后的
比如我们需要分析启动任务的排布是否合理,就可以看第二种图像,可以看到主线程启动任务较多,可能存在一定的长尾效应。
相比systrace,更为轻量