注册

轻量级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,更为轻量

0 个评论

要回复文章请先登录注册