解決 jacoco 合併體積無限膨脹的問題

tlhymm42發表於2024-03-21

背景

目前想每隔 10 分鐘自動收集一次測試環境所有特性分支的當前覆蓋率,並做合併操作,提高覆蓋率收集效率和準確性。
但實際在合併後覆蓋率的檔案會持續膨脹,如果頻繁合併會導致磁碟迅速變滿。

原因分析

分析 merge 邏輯

合併入口:
org.jacoco.core.tools.ExecFileLoader#save

實際寫入:
org.jacoco.core.data.ExecutionDataWriter#visitClassExecution

分析發現,當 append=true,則讀取原來的覆蓋率檔案,併合併到目標覆蓋率檔案中,所以檔案體積會一直變大。

解決辦法

在合併前,先讀取一次原來的檔案,提前做合併操作,待合併完後,不已追加寫的方式寫覆蓋率檔案,而是將覆蓋率重新寫入,這樣寫入的覆蓋率就是合併後的了。
新建一個類,繼承 ExecFileLoader,並覆蓋 save 方法

public void save(final File file, boolean append) throws IOException {
    if (file.exists()) {
        final FileInputStream in = new FileInputStream(file);
        final ExecutionDataReader reader = new ExecutionDataReader(in);

        // 1. 讀取目標覆蓋率檔案
        ExecutionDataStore destExecDataStore = new ExecutionDataStore();
        reader.setSessionInfoVisitor(new SessionInfoStore());
        reader.setExecutionDataVisitor(destExecDataStore);
        reader.read();
        in.close();
        // 2. 合併 新的覆蓋率檔案 和 目標覆蓋檔案
        this.executionData.merge(destExecDataStore.getContents());
    }
    // 3. 不透過追加寫的方式寫入覆蓋率
    final FileOutputStream fileStream = new FileOutputStream(file, false);
    // Avoid concurrent writes from other processes:
    fileStream.getChannel().lock();
    final OutputStream bufferedStream = new BufferedOutputStream(
            fileStream);
    try {
        save(bufferedStream);
    } finally {
        bufferedStream.close();
    }
}

merge 方法:

public void merge(Collection<ExecutionData> executionDataList) {
    Map<Long, ExecutionData> mergeEntries = new HashMap<Long, ExecutionData>();
    // executionDataList 為待合入的覆蓋率檔案
    for (ExecutionData executionData : executionDataList) {
        mergeEntries.put(executionData.getId(), executionData);
    }
    // 
    for (Long id : mergeEntries.keySet()) {
        // this.entries 是後續實際要寫入的覆蓋率資料
        // 1. 如果類id一致,表示類沒有變動,則合併覆蓋率
        if (this.entries.containsKey(id)) {
            ExecutionData merge = mergeEntries.get(id);
            ExecutionData entry = this.entries.get(id);
            entry.mergeProbes(merge.getProbes());
            this.entries.put(id, entry);
        } else {
            // 2. 如果不存在,則有可能類被改過,將舊的覆蓋率合併到新的
            this.entries.put(id, mergeEntries.get(id));
        }
    }
}

mergeProbes 方法

public void mergeProbes(boolean[] probes) {
    for (int i = 0; i < this.probes.length; i++) {
        if (!this.probes[i] && probes[i]) {
            this.probes[i] = probes[i];
        }
    }
}

最後,記得在 org.jacoco.cli.internal.commands.Merge 中,將 ExecFileLoader 替換為我們最佳化後的類

如此,無論是程式碼、覆蓋率有無改動,每次合併都只會合併增量的部分,合併後的檔案大小基本不會發生太大改變。

相關文章