轻量级APP启动信息构建方案

avatar
研发 @字节跳动

作者:大力智能技术团队-客户端 Edden

背景

在头条的启动框架下,启动任务已经划分的较为明确,而启动时序是启动任务中的关键信息。目前我们获取这些信息的主要手段是看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,更为轻量