Mobile Development 10 min read

Design and Implementation of a Lightweight Startup Information Collection and Visualization Framework

This article presents a lightweight framework for constructing, collecting, and visualizing Android startup task information, replacing heavy systrace data with concise logs, Kotlin data structures, AspectJ instrumentation, and Python scripts that generate readable timelines and discrete charts to aid performance analysis.

ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
Design and Implementation of a Lightweight Startup Information Collection and Visualization Framework

Background

Within the existing startup framework, tasks are clearly defined, but the startup sequence is a critical piece of information. The current method of obtaining this information relies on systrace, which has several drawbacks: in release builds it lacks some details (e.g., IO thread information), and the output is heavy and hard to read, making it difficult for developers to investigate and optimize startup tasks.

To address these issues, this article proposes a lightweight solution for describing, collecting, and reconstructing startup information that works flexibly in both release and debug modes, improves readability, and reduces the cost of troubleshooting.

1. Scheme Design

The lightweight startup information construction scheme consists of three parts:

Startup information construction: extracts key information into a new data structure.

Startup information collection: gathers and outputs task information to the reconstruction module.

Startup information reconstruction: builds the information and outputs visual graphs.

2. Specific Module Implementation

2.1 Startup Information Construction

data class InitDataStruct(
    var startTime: Long = 0,
    var duration: Long = 0,
    var currentProcess: String = "",
    var taskName: String = ""
)

The key dimensions of startup information are:

Normalized start time

Duration

Thread (process) name

Task name

Non‑essential tasks that should be filtered out include non‑startup tasks and startup task stacks.

Example JSON format:

{"task_name":"class com.xxx.xxxTask","start_time":5,"duration":9,"current_process":"AA xxxThread#4"}

2.2 Startup Information Collection

Since the company platform is not integrated, the result is output as logs. An AspectJ approach is used to insert hooks around each task: @PointCut("execution(* com.xxx.xxx.xxxTask.run(*))") and then inject code with @Before and @After annotations.

2.3 Startup Information Collection and Rendering

Collected logs are JSON strings like the example above. A Python utility converts them into a readable data structure.

# In the client, JSON is saved

def toInitInfo(json):
    return InitInfo(json["start_time"], json["duration"], json["current_process"], str(json["task_name"]).split('.')[-1])

class InitInfo:
    # startTime and duration are normalized
    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)

    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

All tasks are inserted into a list and sorted by end time:

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)

Two drawing methods are provided:

Method 1 – timeline view where the X‑axis is fixed time intervals and the Y‑axis is the process name.

def drawMp():
    duraLen = 0
    maxLen = 0
    # 10ms interval
    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())
    # draw axis
    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 + "
")
    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 += "
"
        endFile.write(strr)
    endFile.close()

Method 2 – discrete view where the X‑axis is discretized task time and the Y‑axis is the process name.

def drawMp2():
    # discretize time points
    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 = sorted(set(dura))
    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 += "
"
        filee.write(strr)
    filee.close()

3. Effect Comparison

First visualization – startup duration per unit time.

Second visualization – discretized startup time.

By examining the second chart, one can see that the main thread has many startup tasks, indicating a possible long‑tail effect. Compared with systrace, this approach is much lighter.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performancePythonAndroidaspectjprofilingVisualizationstartup
ByteDance Dali Intelligent Technology Team
Written by

ByteDance Dali Intelligent Technology Team

Technical practice sharing from the ByteDance Dali Intelligent Technology Team

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.