View memory snapshot generation from Matrix-ResourceCanary-ForkAnalyseProcessor(1)

As seen earlier, the processing logic of AutoDumpProcessor is mainly to generate, cut hprof files and call back to PluginListener. Next, let’s take a look at the processing logic of ForkAnalyseProcessor.

ForkAnalyseProcessor

public class ForkAnalyseProcessor extends BaseLeakProcessor {<!-- -->

    private static final String TAG = "Matrix.LeakProcessor.ForkAnalyse";

    public ForkAnalyseProcessor(ActivityRefWatcher watcher) {<!-- -->
        super(watcher);
    }

    @Override
    public boolean process(DestroyedActivityInfo destroyedActivityInfo) {<!-- -->
        ...
        getWatcher().triggerGc();

        if (dumpAndAnalyse(
                destroyedActivityInfo.mActivityName,
                destroyedActivityInfo.mKey
        )) {<!-- -->
            getWatcher().markPublished(destroyedActivityInfo.mActivityName, false);
            return true;
        }
        return false;
    }

    private boolean dumpAndAnalyse(String activity, String key) {<!-- -->

        /* Dump */

        final long dumpStart = System.currentTimeMillis();

        File hprof = null;
        try {<!-- -->
            hprof = HprofFileManager.INSTANCE.prepareHprofFile("FAP", true);
        } catch (FileNotFoundException e) {<!-- -->
            MatrixLog.printErrStackTrace(TAG, e, "");
        }

        if (hprof != null) {<!-- -->
            if (!MemoryUtil.dump(hprof.getPath(), 600)) {<!-- -->
                MatrixLog.e(TAG, String.format("heap dump for further analyzing activity with key [%s] was failed, just ignore.",
                        key));
                return false;
            }
        }

        if (hprof == null || hprof.length() == 0) {<!-- -->
            publishIssue(
                    SharePluginInfo.IssueType.ERR_FILE_NOT_FOUND,
                    ResourceConfig.DumpMode.FORK_ANALYSE,
                    activity, key, "FileNull", "0");
            MatrixLog.e(TAG, "cannot create hprof file");
            return false;
        }

        MatrixLog.i(TAG, String.format("dump cost=%sms refString=%s path=%s",
                System.currentTimeMillis() - dumpStart, key, hprof.getPath()));

        /*Analyse */

        try {<!-- -->
            final long analyzeStart = System.currentTimeMillis();

            final ActivityLeakResult leaks = analyze(hprof, key);
            MatrixLog.i(TAG, String.format("analyze cost=%sms refString=%s",
                    System.currentTimeMillis() - analyzeStart, key));

            if (leaks.mLeakFound) {<!-- -->
                final String leakChain = leaks.toString();
                publishIssue(
                        SharePluginInfo.IssueType.LEAK_FOUND,
                        ResourceConfig.DumpMode.FORK_ANALYSE,
                        activity, key, leakChain,
                        String.valueOf(System.currentTimeMillis() - dumpStart));
                MatrixLog.i(TAG, leakChain);
            } else {<!-- -->
                MatrixLog.i(TAG, "leak not found");
            }

        } catch (OutOfMemoryError error) {<!-- -->
            publishIssue(
                    SharePluginInfo.IssueType.ERR_ANALYSE_OOM,
                    ResourceConfig.DumpMode.FORK_ANALYSE,
                    activity, key, "OutOfMemoryError",
                    "0");
            MatrixLog.printErrStackTrace(TAG, error.getCause(), "");
        } finally {<!-- -->
            //noinspection ResultOfMethodCallIgnored
            hprof.delete();
        }

        /* Done */

        return true;
    }
}

From the above code, we can see that in ForkAnalyseProcessor, the memory leak problem discovered is mainly handled through dumpAndAnalyse. Within this function, it is mainly divided into the following steps:

  1. prepareHprofFile: Create hprof file
  2. MemoryUtil.dump: Generate hprof file content
  3. analyze: analyze hprof files
  4. publishIssue: report an issue
  5. hprof.delete(): delete hprof file
prepareHprofFile

In HprofFileManager.INSTANCE.prepareHprofFile is mainly used to pre-create hprof files, including cleaning historical files, ensuring sufficient storage space, determining whether storage space is available, splicing hprof file names, etc. The hprof file pre-created here has no data. Content, prepareHprofFile implementation code is as follows:

@Throws(FileNotFoundException::class)
fun prepareHprofFile(prefix: String = "", deleteSoon: Boolean = false): File {<!-- -->
    hprofStorageDir.prepare(deleteSoon)
    return File(hprofStorageDir, getHprofFileName(prefix))
}
MemoryUtil.dump

The MemoryUtil.dump function mainly completes the work of filling the real content of the hprof file. The code is as follows:

@JvmStatic
@JvmOverloads
fun dump(
    hprofPath: String,
    timeout: Long = DEFAULT_TASK_TIMEOUT
): Boolean = initSafe {<!-- --> exception ->
    if (exception != null) {<!-- -->
        error("", exception)
        return@initSafe false
    }
    return when (val pid = forkDump(hprofPath, timeout)) {<!-- -->
        -1 -> run {<!-- -->
            error("Failed to fork task executing process.")
            false
        }
        else -> run {<!-- --> // current process
            info("Wait for task process [${<!-- -->pid}] complete executing.")
            val result = waitTask(pid)
            result.exception?.let {<!-- -->
                info("Task process [${<!-- -->pid}] complete with error: ${<!-- -->it.message}.")
            } ?: info("Task process [${<!-- -->pid}] complete without error.")
            return result.exception == null
        }
    }
}

private external fun forkDump(hprofPath: String, timeout: Long): Int
private external fun waitTask(pid: Int): TaskResult

You can see that the main logic in the code is to execute forkDump to obtain the process id. If the process id is -1, then false is returned and the dump fails. If the process id is not -1, the waitTask method is executed. If there is no exception in the returned TaskResult object, It means that the dump is successful, otherwise it fails, and forkDump and waitTask are both native methods. Next, let’s take a look at the implementation of these two functions.

forkDump

The native implementation corresponding to MemoryUtil.dump is as follows:

extern "C"
JNIEXPORT jint JNICALL
Java_com_tencent_matrix_resource_MemoryUtil_forkDump(JNIEnv *env, jobject,
                                                     jstring java_hprof_path,
                                                     jlong timeout) {<!-- -->
    const std::string hprof_path = extract_string(env, java_hprof_path);

    int task_pid = fork_task("matrix_mem_dump", timeout);
    if (task_pid != 0) {<!-- -->
        return task_pid;
    } else {<!-- -->
        /* dump generates hprof file */
        execute_dump(hprof_path.c_str());
        /* Exit process */
        _exit(TC_NO_ERROR);
    }
}
static int fork_task(const char *task_name, unsigned int timeout) {<!-- -->
    auto *thread = current_thread();
    // Call art::Dbg::SuspendVM() to suspend the process.
    suspend_runtime(thread);
    // fork creates process
    int pid = fork();
    if (pid == 0) {<!-- -->
        task_process = true;
        if (timeout != 0) {<!-- -->
            alarm(timeout);
        }
        //Set thread name
        prctl(PR_SET_NAME, task_name);
    } else {<!-- -->
        // Call art::Dbg::ResumeVM() to resume the process.
        resume_runtime(thread);
    }
    return pid;
}

Combined with the comments, we can see that this is a piece of code that creates a child process and runs logic based on the child process pid. So how is this piece of code executed?

To understand how the above code is executed, we should first understand the role and characteristics of the child process created by the fork function. For the child process created by fork, it has the same memory space as the parent process. The return value of the fork function is as follows:

image-20230827113118648

It can be seen that fork returns the pid information of the created child process when the parent process is executed, and returns 0 when the child process itself is executed. Combined with the code, the following figure can be obtained:

forkDump.drawio

Next, let’s continue to look at the implementation of sub-process execute_dump and _exit.

execute_dump
static void execute_dump(const char *file_name) {<!-- -->
    _info_log(TAG, "task_process %d: dump", getpid());
    update_task_state(TS_DUMP);
    dump_heap(file_name);
}

static void (*dump_heap_)(const char *, int, bool) = nullptr;

void dump_heap(const char *file_name) {<!-- -->
    dump_heap_(file_name, -1, false);
}

// xhook
load_symbol(dump_heap_,
                void(*)(const char *, int, bool),
                "_ZN3art5hprof8DumpHeapEPKcib",
                "cannot find symbol art::hprof::DumpHeap()")

You can see that execute_dump ultimately calls the art::hprof::DumpHeap() method, and Debug.dumpHprofData ultimately calls this method through jni to generate the hprof file.

_exit

image-20230827123154594

Combining the documentation, we can see that the _exit function is mainly used to stop the process from running.

waitTask
extern "C" JNIEXPORT jobject JNICALL
Java_com_tencent_matrix_resource_MemoryUtil_waitTask(JNIEnv *env, jobject, jint pid) {<!-- -->
    int status;
    //Wait for child process status notification through waitpid
    if (waitpid(pid, & status, 0) == -1) {<!-- -->
        _error_log(TAG, "invoke waitpid failed with errno %d", errno);
        return create_task_result(env, TR_TYPE_WAIT_FAILED, errno, TS_UNKNOWN, "none");
    }

    const int8_t task_state = get_task_state_and_cleanup(pid);
    const std::string task_error = get_task_error_and_cleanup(pid);
    if (WIFEXITED(status)) {<!-- -->
        return create_task_result(env, TR_TYPE_EXIT, WEXITSTATUS(status), task_state, task_error);
    } else if (WIFSIGNALED(status)) {<!-- -->
        return create_task_result(env, TR_TYPE_SIGNALED, WTERMSIG(status), task_state, task_error);
    } else {<!-- -->
        return create_task_result(env, TR_TYPE_UNKNOWN, 0, task_state, task_error);
    }
}

After obtaining the subprocess exit status from waitpid blocking waiting, the subprocess execution result is packaged into a TaskResult object and returned.

waitpid

The description of waitpid is as shown below. You can see that waitpid is used to block the execution of the current thread until it wakes up when the status of the child process associated with the given pid changes. After waking up, it means that the child process has exited. Check the reason for the exit of the child process and return the result to the upper layer. , and the MemoryUtil.dump process ends.

image-20230827124402500

image-20230827124551875