腾讯 Matrix 增量编译 bug 解决,PR 已被官方采访(二)

简介: 腾讯 Matrix 增量编译 bug 解决,PR 已被官方采访

第四次尝试,zip file is empty


通过堆栈信息,报错的地方大概在这里 com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromJar,大概的意思就是 zip file is empty。


这里为了方便,下文统一把 D:\githubRep\gradleLearing\mylibrary\build\intermediates\runtime_library_classes_jar\debug\classes.jar 简称为 classes.jar


对于 transfrom 有一定了解的人,我们都知道 transfrom input 是依赖于上一个 transfrom 的 output 传递过来的,那有没有可能是上一个 transform 传递过来的时候出错。


于是,我去看了我们项目的 transform task,发现还真的存在其他 transfrom,那有没有可能是这个原因呢?(貌似有这个可能呢)


13e5edec97716485dd038ca605ff4952_d753303914428314d6691bd8e4cadead.png


于是,我新建了一个 Demo,确保只有 matrix 的 transfrom,增量编译,启动。。。。。


可惜,还是黑屏,那么,到这里,可以确定的是,一定是 matrix transfrom 的问题。这再次加强了我去看 matrix trace plugin 代码的决心。


看到这里,我们可能有点乱了?


我们先来梳理一下,开启增量编译之后, ClassNotFound 的问题基本可以确定是 trace plugin 插件引起的,而 class.jar 大小 size 为 0,那么很有可能在处理 class.jar 的时候出错了

带着这个怀疑,我们来看他们的调用关系, MethodTracer#innerTraceMethodFromJar(File input, File output) 的 input jar size 为 0 ,梳理它的调用逻辑,如下


com.tencent.matrix.plugin.trace.MatrixTrace#doTransform
methodTracer.trace(dirInputOutMap, jarInputOutMap) // dirInputOutMap 这里传递过去的
com.tencent.matrix.trace.MethodTracer#trace
com.tencent.matrix.trace.MethodTracer#traceMethodFromJar
com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromJar(File input, File output)


这里,我们主要关注一下 MatrixTrace#doTransform 方法里面的 methodTracer.trace(dirInputOutMap, jarInputOutMap),因为 input 就是从这里传递过去的。


fun doTransform(classInputs: Collection<File>,
                    changedFiles: Map<File, Status>,
                    inputToOutput: Map<File, File>,
                    isIncremental: Boolean,
                    traceClassDirectoryOutput: File,
                    legacyReplaceChangedFile: ((File, Map<File, Status>) -> Object)?,
                    legacyReplaceFile: ((File, File) -> (Object))?
    ) {
        // 省略若干代码
        /**
         * step 1
         */
        var start = System.currentTimeMillis()
        val futures = LinkedList<Future><*>>()
        val mappingCollector = MappingCollector()
        val methodId = AtomicInteger(0)
        val collectedMethodMap = ConcurrentHashMap<String, TraceMethod>()
        futures.add(executor.submit(ParseMappingTask(
                mappingCollector, collectedMethodMap, methodId, config)))
        // dirInputOutMap 在这里初始化
        val dirInputOutMap = ConcurrentHashMap<File, File>()
        val jarInputOutMap = ConcurrentHashMap<File, File>()
        for (file in classInputs) {
            if (file.isDirectory) {
                futures.add(executor.submit(CollectDirectoryInputTask(
                        directoryInput = file,
                        mapOfChangedFiles = changedFiles,
                        mapOfInputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassDirectoryOutput = traceClassDirectoryOutput,
                        legacyReplaceChangedFile = legacyReplaceChangedFile,
                        legacyReplaceFile = legacyReplaceFile,
                        // 第一个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap
                )))
            } else {
                val status = Status.CHANGED
                futures.add(executor.submit(CollectJarInputTask(
                        inputJar = file,
                        inputJarStatus = status,
                        inputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassFileOutput = traceClassDirectoryOutput,
                        legacyReplaceFile = legacyReplaceFile,
                        // 第二个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap,
                        resultOfJarInputToOut = jarInputOutMap
                )))
            }
        }
        for (future in futures) {
            future.get()
        }
        futures.clear()
        Log.i(TAG, "[doTransform] Step(1)[Parse]... cost:%sms", System.currentTimeMillis() - start)
        /**
         * step 2
         */
        start = System.currentTimeMillis()
        val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
        methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)
        Log.i(TAG, "[doTransform] Step(2)[Collection]... cost:%sms", System.currentTimeMillis() - start)
        /**
         * step 3
         */
        start = System.currentTimeMillis()
        val methodTracer = MethodTracer(executor, mappingCollector, config, methodCollector.collectedMethodMap, methodCollector.collectedClassExtendMap)
        // 第三个地方,可能修改 dirInputOutMap 的值
        methodTracer.trace(dirInputOutMap, jarInputOutMap)
        Log.i(TAG, "[doTransform] Step(3)[Trace]... cost:%sms", System.currentTimeMillis() - start)
    }


主要关注可能修改 dirInputOutMap 的地方,上面的代码已经标注出来了,可以看到,主要有三个地方可能修改。


于是,我加上断点,断点的地方分别在 step1, step2 ,step3 注释的地方,debug 了一下


  • step1 的时候 classes.jar 大小不为 0
  • step2 的时候 classes.jar 大小不为0
  • step3 的时候 classes.jar 大小不为 0


这里可能会有人有这样的疑问,为什么是看 D:\githubRep\gradleLearing\mylibrary\build\intermediates\runtime_library_classes_jar\debug\classes.jar 这个文件,因为我们报错的堆栈,是这个 class.jar 大小为 0.


44fcca2e29ded23875fafac19c6bb9d4_9499dcdc51300501615c948e3fa28b8b.png


既然这三个地方都不为 0,那么很有可能,是在 methodTracer.trace(dirInputOutMap, jarInputOutMap) 方法 中修改了。


public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList) throws ExecutionException, InterruptedException {
        List<Future> futures = new LinkedList<>();
        traceMethodFromSrc(srcFolderList, futures);
        traceMethodFromJar(dependencyJarList, futures);
        for (Future future : futures) {
            future.get();
        }
        futures.clear();
}
private void traceMethodFromSrc(Map<File, File> srcMap, List<Future> futures) {
        if (null != srcMap) {
            for (Map.Entry<File, File> entry : srcMap.entrySet()) {
                futures.add(executor.submit(new Runnable() {
                    @Override
                    public void run() {
                        innerTraceMethodFromSrc(entry.getKey(), entry.getValue());
                    }
                }));
            }
        }
    }


trace 方法主要执行了两个逻辑


  • 执行 traceMethodFromSrc
  • 执行 traceMethodFromJar 方法


而我们的 dirInputOutMap 参数对应的 trace 方法的 srcFolderList 参数,于是,我们在 innerTraceMethodFromSrc 方法的开始和结束的地方,设置条件断点,条件是 input.path.equals("D:\\githubRep\\gradleLearing\\mylibrary\\build\\intermediates\\runtime_library_classes_jar\\debug\\classes.jar")


05d2daad3e36c1c5e68df3047fa9cd87_e0c419f44964cc06e918d914bce3dfe9.png


debug 发现,在刚开始调用 innerTraceMethodFromSrc 方法的时候(这个方法很重要,下文还会涉及到),我们的 classes.jar文件大小不为 0,可以等到方法执行完成的时候, classes.jar 文件大小为 0。

这时候基本可以确定了是 innerTraceMethodFromSrc 方法修改了 classes.jar,导致大小为 0.


innerTraceMethodFromSrc 方法,可以看到有两个地方操作了文件


  • FileUtil.copyFileUsingStream(classFile, changedFileOutput)
  • Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)


private void innerTraceMethodFromSrc(File input, File output) {
        ArrayList<File> classFileList = new ArrayList<>();
        if (input.isDirectory()) {
            listClassFiles(classFileList, input);
        } else {
            classFileList.add(input);
        }
        for (File classFile : classFileList) {
            InputStream is = null;
            FileOutputStream os = null;
            try {
                final String changedFileInputFullPath = classFile.getAbsolutePath();
                final File changedFileOutput = new File(changedFileInputFullPath.replace(input.getAbsolutePath(), output.getAbsolutePath()));
                if (!changedFileOutput.exists()) {
                    changedFileOutput.getParentFile().mkdirs();
                }
                changedFileOutput.createNewFile();
                if (MethodCollector.isNeedTraceFile(classFile.getName())) {
                    is = new FileInputStream(classFile);
                    ClassReader classReader = new ClassReader(is);
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    is.close();
                    if (output.isDirectory()) {
                        os = new FileOutputStream(changedFileOutput);
                    } else {
                        os = new FileOutputStream(output);
                    }
                    os.write(classWriter.toByteArray());
                    os.close();
                } else {
                     // 这里 对文件进行操作,当 classFile 和 changedFileOutput 路径相同时,导致 `classes.jar` 为 0
                    FileUtil.copyFileUsingStream(classFile, changedFileOutput);
                }
            } catch (Exception e) {
                Log.e(TAG, "[innerTraceMethodFromSrc] input:%s e:%s", input.getName(), e);
                try {
                   // 这里 对文件进行操作
                    Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING);
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            } finally {
                try {
                    is.close();
                    os.close();
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }

进行条件断点的时候,发现是 FileUtil.copyFileUsingStream 进行 copy 的时候,因为同时读写一个文件,导致 classes.jar 被更改,内容被抹除。到此,原因已经找到了,即 dirInputOutMap 中 input 和 output file 文件路径一致,导致内容错误,那要怎么解决?


小幸运,终于找到解决方案


前面我们说到 dirInputOutMap 中 input 和 output file 文件路径一致,导致内容错误。


那一个最直观的方式,我们尝试加上这样的条件,当 classFile 和 changedFileOutput 路径一致的时候,不进行 copy。


if (!classFile.getAbsolutePath().equals(changedFileOutput.getAbsolutePath())) {
    FileUtil.copyFileUsingStream(classFile, changedFileOutput
} else {
    Log.e(TAG, "error, name should not be equal, classFile.getAbsolutePath() is "+ classFile.getAbsolutePath());
}

编译本地 matrix trace plugin 版本,运行 demo,跑起来,你会发现 App 正常了,不会 crash 了。too young,too simple.


a6ba306606ead86ac337b53cc4ddbda0_419dc9f347570eab6d46ebdb593fc9f4.png


但是这样会带来一个新的问题,增量编译的时候,不进行 copy,那我们代码的变动,永远不会生效。所以,还是得找为什么 dirInputOutMap 中 input 和 output file 的路径是一样的


还记得前面的 MatrixTrace#doTransform 方法嘛,我们来看一下 step1 和 step2 之间执行的代码


for (file in classInputs) {
            if (file.isDirectory) {
                futures.add(executor.submit(CollectDirectoryInputTask(
                        directoryInput = file,
                        mapOfChangedFiles = changedFiles,
                        mapOfInputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassDirectoryOutput = traceClassDirectoryOutput,
                        legacyReplaceChangedFile = legacyReplaceChangedFile,
                        legacyReplaceFile = legacyReplaceFile,
                        // 第一个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap
                )))
            } else {
                val status = Status.CHANGED
                futures.add(executor.submit(CollectJarInputTask(
                        inputJar = file,
                        inputJarStatus = status,
                        inputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassFileOutput = traceClassDirectoryOutput,
                        legacyReplaceFile = legacyReplaceFile,
                        // 第二个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap,
                        resultOfJarInputToOut = jarInputOutMap
                )))
            }
        }
        for (future in futures) {
            future.get()
        }
        futures.clear()


可以看到,这个方法主要干了两件事情


  • 遍历文件,如果 isDirectory 为 true, 执行 CollectDirectoryInputTask 任务
  • 如果是文件,执行 CollectJarInputTask 任务


我们先来看一下 CollectDirectoryInputTask 类,因为我们主要是关注 dirInputOutMap,我们 find usage 一下,发现 dirInputOutMap 在 com.tencent.matrix.plugin.trace.MatrixTrace.CollectDirectoryInputTask#handle 更改


f00d45f9d3940924fb4e1ab53e14532d_847457a1df72ed6c0dbd9a11becf7e23.png


因为是增量编译出现问题,所以,我们在 isIncremental 为 true 的时候设置断点,断点条件为 changedFileInput.absolutePath.equals("D:\\githubRep\\gradleLearing\\mylibrary\\build\\intermediates\\runtime_library_classes_jar\\debug\\classes.jar")


6eb472e481f4ad8dc501b308ac14d7c7_c0fb062f4b42f5acb475f835a398e2e0.png


很快我们发现 changedFileInput 和 changedFileOutput 的路径是是一模一样的,即 resultOfDirInputToOut[changedFileInput] = changedFileOutput 中 resultOfDirInputToOut key 和 value 是一致的,那么很有可能就是这个原因。


于是,我对代码进行了修改,将  val changedFileOutput = File(changedFileInputFullPath.replace(inputFullPath, outputFullPath)) 修改为如下的代码。


val changedFileOutput = if (changedFileInputFullPath.contains(inputFullPath)){
                        File(changedFileInputFullPath.replace(inputFullPath, outputFullPath))
                    } else { // if not contains, changedFileOutput should be modify, else when we read and write the same file, the jar would be empty
                        File(outputFullPath, changedFileInput.name)
                    }


本地编译 matrix trace plugin,发现完美运行,不管是全量编译,还是增量编译, perfect。到此问题终于解决了。


至于项目中 val changedFileOutput = File(changedFileInputFullPath.replace(inputFullPath, outputFullPath)) 的这行代码,我猜测可能跟 AGP 早期的版本有关吧,可能早期,inputFullPath 的路径一定是包含在 changedFileInputFullPath 里面的,然后就写了这样的代码,后面 AGP 升级,导致增量编译有问题,具体的没验证,猜测而已。


小结


其实,这次解决问题的过程我算是挺幸运的,能找到解决方案。很多时候,有一些疑难杂症,排查了好久,都没法找到根本原因。有结果当然是最好的,没有的话,其实我们也有很大收获,在这过程中我们培养了独立解决问题的能力,这对我们自身的成长有莫大的帮助。


再来简述一下这次历程,这一次,调试 matrix trace plugin 插件,刚开始真的是一脸懵逼。一会编出来的包,有问题,一会没有问题。


于是在本地尝试了好久,终于发现了复现路径,然后到 issue 上面也搜了一下,发现很多人遇到这个问题,但是还没有解决。


于是,就先关了 trace 插件的增量编译,发现 OK 了。但是这只是一个规避方案,不是一个解决方案。那时候,还比较忙,看了一天左右,也没找出原因,一脸懵逼。就先去加入 matrix 功能了。


可是,这个问题却一直在脑海中记着,过了三四天,差不多接入完成了。就硬着头条去看源代码了。真的没有捷径,一步步排查,刚开始的时候,总想着一步到位,想一口吃成胖子,看能不能一下子解决,看着看着就绕晕了。后面我就学乖了,一步步来,一步步调试,逐个排查,最终,运气比较好,终于找到原因了。


那一刻,真的是挺开心的,充满满满的成就感。


可以看到,这次我解决问题的思路是:


搜索有没有类似的问题 -》 尝试复现路径 -》 再次搜索类似的问题 -》 最小版本验证是增编编译的问题 -》 从日志找出关键信息 -》 根据错误信息一步步排查 -》 定位到原因 -》 一步步找到解决方案。


你学废了吗?如果是你,你会怎么解决呢?有更好的方案嘛,欢迎留言讨论。


pull request 地址, 提了 pr,官方暂时还没有处理,到时候不知道会不会打脸,哈哈。


相关文章
|
人工智能
Let’s Make-It-3D!上交&微软最新开源2D转3D生成研究,Star超过1k星
Let’s Make-It-3D!上交&微软最新开源2D转3D生成研究,Star超过1k星
362 0
|
2月前
Force Yc团队最新第五次创作引导页源码
Force Yc团队最新第五次创作引导页源码 此源码可以播放自己的音乐 无法播放视频背景!~ 音乐修改:music 音乐名称:bgm.mp3 LOGO修改:images 图片名字:top-logo.mp4 文本修改:index.html Notepad++编辑
43 10
Force Yc团队最新第五次创作引导页源码
|
3月前
|
自然语言处理 搜索推荐 程序员
因为看不惯Notepad++,国内大佬开源了Notepad--:技术分享与工作学习中的新选择
【8月更文挑战第20天】在编程界,文本编辑器是每一位开发者日常工作中不可或缺的工具。Notepad++,这款曾经风靡一时的文本编辑器,以其强大的功能和简洁的界面赢得了众多程序员的喜爱。然而,近年来,由于其作者的一些不当言论和行为,引发了广泛争议,许多程序员开始寻找替代品。在这样的背景下,国内一位大佬挺身而出,开源了Notepad--,为开发者们带来了一个新的选择。
379 1
|
4月前
|
机器人 vr&ar 计算机视觉
|
Dart JavaScript Java
腾讯 Matrix 增量编译 bug 解决,PR 已被官方采访(一)
腾讯 Matrix 增量编译 bug 解决,PR 已被官方采访
|
存储 自动驾驶 API
十年积累,5.4万GitHub Star一朝清零:开源史上最大意外损失
十年积累,5.4万GitHub Star一朝清零:开源史上最大意外损失
261 0
|
Prometheus Cloud Native IDE
名垂千古的机会到了,一文说清【给开源大项目贡献代码】二三事(github,pr,fork,ci)
名垂千古的机会到了,一文说清【给开源大项目贡献代码】二三事(github,pr,fork,ci)
名垂千古的机会到了,一文说清【给开源大项目贡献代码】二三事(github,pr,fork,ci)
|
安全 Java 程序员
Gitee 关闭部分开源仓库;大厂一半以上程序员愿意降薪跳槽;Deno 1.22 发布 | 思否周刊
Gitee 关闭部分开源仓库;大厂一半以上程序员愿意降薪跳槽;Deno 1.22 发布 | 思否周刊
227 0
|
Web App开发 安全 Ubuntu
曝 iPhone 14 没有 mini 版本;百度员工跳槽字节被判赔 107 万元;Firefox 100 发布 | 思否周刊
曝 iPhone 14 没有 mini 版本;百度员工跳槽字节被判赔 107 万元;Firefox 100 发布 | 思否周刊
165 0
|
Web App开发 SQL 安全
项目作者操作不当,5.4 万 Star 归零;Go 1.18.1 发布 | 思否周刊
项目作者操作不当,5.4 万 Star 归零;Go 1.18.1 发布 | 思否周刊
140 0