当前位置:网站首页>Optimal solution for cold start

Optimal solution for cold start

2022-06-25 12:41:00 I'm Wong Tai Sin

1. background

A while ago, I made a demand , This is to solve the problem of slow startup of one of our modules , After investigation, it is found that the task execution time of our core path is relatively long . We came up with an optimization method , Is in the App When starting, start a low priority thread to preload tasks , When users really use this module , The startup time will be greatly shortened .

However, in the application mr When , Questioned by the basic colleagues , It is no longer allowed to add tasks in the startup phase , If you have to add , You must apply by email . That's how it feels to me , This App You didn't write it , You can't realize what you want ( Actually, the experience was terrible ), Maybe it will be like this when the team is big , Then we found another time to preload , Avoid adding tasks in the startup phase .

In the process of solving this problem , I found out , Our task startup code is poorly written , This reminds me of the previous cold start optimization , Do a startup framework , It can help us reasonably arrange the startup task , And monitor the time of each task and the overall execution time , Prevent deterioration .

I reuse it kotlin Write it over , Share in github On , I named it StartUp .
https://github.com/bearhuang-omg/startup

2. Usage mode

Before introducing how to use , You need to know the following classes first :
Several important classes :

class explain
TaskDirector Task director class , Depending on the task interdependencies and priorities , Arrange the execution sequence of tasks , It's also sdk Entrance
Priority Define the priority of the task , There are four priorities , Namely IO( stay io Execute on the thread pool ),Calculate( Execute on the compute thread pool ),Idle( Execute when idle ),Main( Main thread execution )
Task The task class , Priority can be specified , Specify the task name on which it depends , Be careful : The task name cannot be duplicate
IDirectorListener The life cycle of task execution , Include :onStart , onFinished,onError

After resealing , The interface provided is very simple
Usage mode :

Interface Parameters Return value remarks
addTasktask:Task // Mission TaskDirector return TaskDirector, You can add tasks in a chain
registerListenerIDirectorListener nothing Monitor task execution
unRegisterListenerIDirectorListener nothing Unregister listening
start nothing nothing Start task execution

Example :

// Create tasks 
val task2 = object:Task() {
    
    override fun execute() {
    
        Thread.sleep(1000)
    }

    override fun getName(): String {
    
        return "tttt2"
    }

    override fun getDepends(): Set<String> {
    
        return setOf("tttt1")
    }
}

// Create task Director 
val director = TaskDirector()
// Listening task lifecycle 
director.registerListener(object : IDirectorListener{
    
    override fun onStart() {
    
        Log.i(TAG,"director start")
    }

    override fun onFinished(time: Long) {
    
        Log.i(TAG,"director finished with time ${
      time}")
    }

    override fun onError(code: Int, msg: String) {
    
        Log.i(TAG,"errorCode:${
      code},msg:${
      msg}")
    }
})
// Add tasks 
director.apply {
    
    addTask(task1)
    addTask(task2)
    addTask(task3)
}
// Start execution 
director.start()

3. The basic principle

We used to do cold start optimization , Some pain points in the start-up phase are summarized :

  1. The code is a mess , Can not clearly know what is necessary , What is not necessary ;
  2. If the task has dependencies , If you don't add notes , It's easy to be modified by those who follow , Make a mistake ;
  3. It is impossible to know exactly how long the task will take , It is difficult to determine the optimization direction ;

For these pain points , We did the following :

1. Abstract it into a task diagram

We encapsulate the relatively independent processes in the startup phase into individual processes task, And you can specify its priority and task dependency , If there is no dependency, it is directly attached to the root node .
such as , There are ABCDE Five tasks , among A,B Does not depend on any task ,C Depend on A,D Depend on AB,E Depend on CD. Therefore, the generated task graph is as follows :
 Insert picture description here

Creating task dependencies is also very simple , With ABC For example :

// Create tasks A
val taskA = object:Task() {
    
    override fun execute() {
    
    }

    override fun getName(): String {
    
        return "A"
    }
}
// Create tasks B
val taskB = object:Task() {
    
    override fun execute() {
    
    }

    override fun getName(): String {
    
        return "B"
    }
}
// Create tasks C
val taskC = object:Task() {
    
    override fun execute() {
    
    }

    override fun getName(): String {
    
        return "C"
    }

    // Depending on the task A and B
    override fun getDepends(): Set<String> {
    
        return setOf("A","B")
    }
}

among getName Methods do not have to be duplicated , If there is no replication , The framework will automatically generate a unique name.

2. Check whether there are rings

When the task graph is generated , Naturally, we will encounter the following two problems :

  1. The dependent task is not in the task diagram ;
  2. There are rings in the generated task graph ;

First, let's look at the first question :
Every time we call addTask After the interface , The framework saves the task in the task map among , among key For the task name, If you find that the task you depend on is not map among , Will immediately call back the lifecycle onError Interface .

// Mission map
private val taskMap = HashMap<String, TaskNode>()
// Life cycle onError Interface 
fun onError(code: Int, msg: String)

Let's look at the second question ,
If there are rings in the task diagram , Then the tasks will be interdependent , The task cannot be executed correctly .
There are rings in the task diagram , It can be divided into the following two cases :

1. The task ring is independent of Root Outside the node
 Insert picture description here

2. The task loop is not independent of Root Outside the node
 Insert picture description here

The main inspection process is as follows :

  1. from Root Node departure , Add non dependent tasks to the queue in turn ;
  2. Each time the current task is taken from the queue and moved out of the queue , Reduce the number of dependencies of its subtasks 1, If the number of dependencies of its subtasks is less than or equal to 0, The subtask is also added to the queue ;
  3. repeat 2, Until the queue is empty ;
  4. If the task loop is not independent of Root node , In the process of traversal, a task has been moved out of the queue , A subsequent subtask adds it to the queue , At this point, it can be judged that there is a ring ;
  5. If the task ring is independent of Root node , After the queue is empty , There are still tasks that have not been traversed .
  6. If not 4,5 Two cases , Then it can be determined that there is no ring in the task diagram .

The code implementation is as follows :

private fun checkCycle(): Boolean {
    
    val tempQueue = ConcurrentLinkedDeque<TaskNode>() // The record has been ready The task of 
    tempQueue.offer(rootNode)
    val tempMap = HashMap<String, TaskNode>() // All current tasks 
    val dependsMap = HashMap<String, Int>() // The number of tasks on which all tasks depend 
    taskMap.forEach {
    
        tempMap[it.key] = it.value
        dependsMap[it.key] = it.value.task.getDepends().size
    }
    while (tempQueue.isNotEmpty()) {
    
        val node = tempQueue.poll()
        if (!tempMap.containsKey(node.key)) {
    
            Log.i(TAG, "task has cycle ${
      node.key}")
            directorListener.forEach {
    
                it.onError(Constant.HASH_CYCLE, "TASK HAS CYCLE! ${
      node.key}")
            }
            return false
        }
        tempMap.remove(node.key)
        if (node.next.isNotEmpty()) {
    
            node.next.forEach {
    
                if (dependsMap.containsKey(it.key)) {
    
                    var dependsCount = dependsMap[it.key]!!
                    dependsCount -= 1
                    dependsMap[it.key] = dependsCount
                    if (dependsCount <= 0) {
    
                        tempQueue.offer(it)
                    }
                }
            }
        }
    }
    if (tempMap.isNotEmpty()) {
    
        Log.i(TAG, "has cycle,tasks:${
      tempMap.keys}")
        directorListener.forEach {
    
            it.onError(Constant.HASH_CYCLE, "SEPERATE FROM THE ROOT! ${
      tempMap.keys}")
        }
        return false
    }
    return true
}

3. Let tasks execute in different thread pools

After the task check is legal , Then you can start to execute happily , Different tasks will be thrown to different thread pools for execution according to their priority .

when (task.getPriority()) {
    
    Priority.Calculate -> calculatePool.submit(task)
    Priority.IO -> ioPool.submit(task)
    Priority.Main -> mainHandler.post(task)
    Priority.Idle -> {
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
            Looper.getMainLooper().queue.addIdleHandler {
    
                task.run()
                return@addIdleHandler false
            }
        } else {
    
            ioPool.submit(task)
        }
    }
}

After each task is completed , It will automatically trigger the execution of its subtasks , The subtask will determine the number of dependencies of the current task , When the dependent quantity is 0 when , It can be really implemented .
There is actually a concurrency problem here , For example, tasks C Depend on A and B, and A and B Execute on different threads , When A and B After the execution is complete , Trigger at the same time C perform , May lead to inconsistent changes in the number of dependencies , Problems arise . The previous solution was to lock , Now I put all these scheduled tasks in TaskDirector Execute in a separate thread in , Avoid the problem of concurrency , And there is no need to lock .

private fun runTaskAfter(name: String) {
    
    // TaskDirector Independent threads of , Avoid concurrent problems 
    handler.post {
    
        finishedTasks++
        // Record the time when the task was executed 
        if (timeMonitor.containsKey(name)) {
    
            timeMonitor[name] = System.currentTimeMillis() - timeMonitor[name]!!
        }
        // After the execution of a single task , Trigger the next task execution 
        if (taskMap.containsKey(name) && taskMap[name]!!.next.isNotEmpty()) {
    
            taskMap[name]!!.next.forEach {
     taskNode ->
                taskNode.start()
            }
            taskMap.remove(name)
        }
        Log.i(TAG, "finished task:${
      name},tasksum:${
      taskSum},finishedTasks:${
      finishedTasks}")
        // After all tasks are completed , Trigger director Callback 
        if (finishedTasks == taskSum) {
    
            val endTime = System.currentTimeMillis()
            if (timeMonitor.containsKey(WHOLE_TASK)) {
    
                timeMonitor[WHOLE_TASK] = endTime - timeMonitor[WHOLE_TASK]!!
            }
            Log.i(TAG, "finished All task , time:${
      timeMonitor}")
            runDirectorAfter()
        }
    }
}

4. Monitoring and anti degradation

Preventing deterioration is a very important problem , We worked hard to optimize for a long time , It turned out that there were few versions , The start-up time has slowed down again , This is too big for me .
For each task, We all added monitoring , Automatically monitor the execution time of each task , And the overall execution time of all tasks . After the task is completed , Report at a certain time , In this way, the startup process can be monitored from time to time .

abstract class Task : Runnable {
    

    ......

    final override fun run() {
    
        Log.i(TAG,"start ${
      getName()}")
        before.forEach {
    
            it(getName())
        }
        execute()
        after.forEach {
    
            it(getName())
        }
        Log.i(TAG,"end ${
      getName()}")
    }

    ......

}

4. summary

The cold start scenario has a great impact on the user experience , It's also very good to have a dedicated colleague monitoring on the basic side , But I think the important thing is to be sparse , Instead of blocking , How to load on demand is what we pursue , Not one size fits all , Directly remove the top leaders , Let the business side be tied up .

原网站

版权声明
本文为[I'm Wong Tai Sin]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/176/202206251206114506.html