方法论:在不是太懂源码的情况下,我是怎么定位源码问题的?

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 方法论:在不是太懂源码的情况下,我是怎么定位源码问题的?

在日常开发中,我们多多少少会遇到些问题,有时候是自己的写法有错误,这时候可能就要先检查一遍,看看文档,看看是哪里的问题。

但有时候也有可能是框架/工具的源码错误,虽然一般这种情况很少发生,因为一般框架/工具都会做了比较多的单元测试,经过开源社区的验证,出错的概率比较少,但也不一定所有情况都能测试到。

那么,如果真的认为是源码的 Bug,我们该怎么去定位呢?

本篇文章讲解介绍我最近遇到的一个真实例子,并通过自己的一些经验、调试技巧,去定位问题


发现问题


在我的某个项目中,当我使用 pnpm i --fix-lockfile 时,一定会报如下错误:

1686401625626.png

运行 pnpm i 的时候,不会报错,只有运行  pnpm i --fix-lockfile 会报错。在一些业务场景下,我们偏向于使用   pnpm i --fix-lockfile,当然我也可以改为用 pnpm i,那故事就结束了,全剧终hhh。

当然我还是稍微努力了一下下,准备提个 issue 看看。

既然要提 issue,那就得首先觉得它是  pnpm 自身的问题,不是我写的代码有问题。我个人主要是有以下原因:

  • 我就是安装个依赖,这能有什么错哦。。。而且它 pnpm i 是能安装的
  • --fix-lockfile 这个选项,肯定比仅仅使用 pnpm i 的场景少,那在极端场景下,可能 pnpm 的单元测试没覆盖到,有问题也是正常的
  • 我是学过英文的,错误信息很明显就说,vite@4.0.4_@types+node@17.0.45 这个版本解析不出来,个人感觉应该是要解析成 vite 4.0.4,我 package.json 也是这么写的,pnpm 自己加的其他东西,那肯定不关我的事情呀。而且 pnpm 的 lock 文件也是用 vite@4.0.4_@types+node@17.0.45,那是你的问题没错了
  • 错误信息中出现 @vitejs/plugin-basic-ssl,有可能是这个包不行,但 pnpm i 既然能正常安装,就证明人家本身没问题,是 pnpm 的问题。

因此,我提了个 issue,就贴了个截图,然后写 pnpm i --fix-lockfile 安装失败,是解析版本失败了,还贴了 pnpm 的锁文件。

我觉得我已经写得很明白了,这么一个 package 的版本解析错误问题,作者应该一看就懂。。。了吧

结果不出所料,作者也看不懂,让我提供一个最小的复现 Demo

这里补充一个知识点,一般提 issue 的时候,都要带上最小的复现 Demo,不然人家作者也没办法复现你的问题。

但是鸭,很多时候,开发者可能遇到问题了,却提供不出来,主要有以下原因:

  • 项目非常大,不知道哪里有问题,因此不知道怎么做一个最小复现的 Demo
  • 是公司的项目,不能将代码提供出去

我是两个原因都有,因此不是我不想提供 Demo,而是我也搞不出来。。。因此想碰碰运气,说不定作者一看就知道呢hhh,结果不出所料,还是得提供 Demo。

很多人提供不了最小复现 Demo,开源库作者也没办法知道问题,然后问题就不了了之。

因此,很多人也只能走到这一步,然后故事就结束了。

但其实不是完全不可能提供一个 Demo,看要不要再努力一下下。这时候人和人的差别就会显现出来了

  • 有的人可能觉得换一种方式就行了
  • 有的人可能觉得没多大影响,不折腾了
  • 有的人可能觉得,我就是要搞出来。

当我第一次遇到这个问题的时候,我也是抱着,算了不管了

后来再遇上,真烦,不如提个 issue 碰碰运气吧

再后来多遇上几次,实在不想忍了,晚上调试一下看看,就花一个晚上,不行拉倒

因此才有了接下来的一些努力。

有时候,你离开源贡献,就只有一念之差。只是,有些人选择放弃,有的人选择再努力一下。


调试代码


光有决心还是没有的,得实际行动。

但一个巨大的问题摆在面前,pnpm 的代码我也没看过鸭,调个啥玩意???

因此,第一个问题,是怎么把 pnpm 源码跑起来调试呢?


pnpm 源码调试


之前看了神光大佬的调试小册,学到了很多调试相关的知识,感兴趣的可以学习一下

一般情况下,如何知道一个开源仓库要怎么进行调试呢?

  1. 看仓库的 CONTRIBUTING.md 文档,按道理比较常见的开源仓库都会有
  2. 找别人总结过调试文章

我随便在掘金,找了一遍文章,毕竟能调试,能打断点就行。因此如何调试的问题就解决了。

这里总结一下:

  • pnpm i 先安装 pnpm 源码的依赖
  • pnpm run compile,执行源码所有包的构建(pnpm 是 monorepo 仓库)
  • 用 node 执行 pnpm 的入口脚本

下图是我在 webstorm 的调试配置,qf-tds-vue-plugins 是我的项目文件夹,下面配置的意思是,我要在这个文件夹运行以下命令(因为是在项目目录安装依赖):


# 实际上 pnpm i,也是运行全局安装的 pnpm 目录下的 bin/pnpm.cjs
node /candy/app/pnpm/pnpm/bin/pnpm.cjs i --fix-lockfile

1686401602405.png

找个地方打个断点,代码能停住(停不住可能是根本没运行这行代码,换个别的),就代表这一步已经成功了


定位问题


这一步才是最核心、但又最麻烦的步骤

**如何在茫茫源码中定位问题?**下面是我的一些个人经验:

从错误信息出发,找到报错的代码


1686401587626.png


我们全局搜索关键字isn't supported by any available resolver,找到是哪一行报错的,找到之后,打个断点。

1686401575211.png

这就找到了报错源头了。因为 resolution 不为真值,所以报错了,那我们的问题就变成了,为什么  resolution 不为真值。这就将很大很抽象的问题,转化成了一个更小更明确的问题

resolution 是由 resolveFromNpm 返回的,那我们就修改一下断点位置

这里有一个小经验,**断点位置要改到哪里比较好?**有两种方式:

  • 找到 resolveFromNpm 的函数源码实现,在函数实现里面打断点
  • 直接在  resolveFromNpm 函数调用的位置打断点。

我个人更偏向与在调用的位置打断点,因为更方便。可以看上图的例子,resolveFromNpm 是另一个函数返回的,如果你想要找到它的实现,还得进去 createNpmResolver 函数里面找,说不定里面函数比较复杂,就比较麻烦,需要找到 resolveFromNpm 函数真正的内部实现,才能打断点 。

如果是在调用位置打断点,就会在 resolveFromNpm 函数调用前停住,此时,我们按进入函数,就能直接找到源码了

1686401562874.png

因此断点会改到这里,但我们运行后会发现,每个 package 都会在这里暂停,一个项目这么多包,不行啊。

这时候就要用到条件断点如何设置条件断点呢?可以先观察一下一些变量的值

1686401552612.png

可以看到 wantedDependency.pref 的值为 4.0.4_@types+node@17.0.45,那就用这个了。断点的条件就设置为


wantedDependency.pref === '4.0.4_@types+node@17.0.45'

这就能在出错前将代码定住了,然后我们进入函数


1686401534256.png

进入 resolveFromNpm 调试,然后发现 spec 为 null,所以函数 return null 了,因此又可以将问题转化:为什么 spec 会使 null?

那就要排查 parsePref 函数了,还是用上述的思路,打断点,进入函数,

同样的,按照上述思路就是 parsePref 函数的问题了,这里就不重复了。

最后发现,是 wantedDependency.pref 这个属性,应该为 4.0.4,才能使后面的代码不报错,而不是 4.0.4_@types+node@17.0.45

那接下来的问题就转化成了: wantedDependency.pref 为什么不为 4.0.4?我们需要找到 wantedDependency.pref 被赋值的地方

下面又是一些经验:

  • 全局搜索 .pref =,是为了所有出 wantedDependency.pref = xxx 的这些代码
  • 全局搜索 pref:(注意前面有空格),这个是为了搜索 { pref: xxx } 的代码

不过很可惜,在 pnpm 中都搜不到太多有用的信息,那就只能通过调试找了。接下来该怎么办呢?

1686401521919.png

我们可以利用函数的调用栈,逐级往上找,调试方法跟之前一样,目标是,找到 wantedDependency.pref 被赋值的地方。

有较多调试经验的开发者,也可以不逐级网上找,如果觉得肯定不会在当前函数层级被赋值,可以直接跳到更深的函数调用层级中

最终,我找到了整个 wantedDependency 初始化的地方:resolveDependency 函数。

这里我直接回顾一下整个错误的相关信息:

  1. @vitejs/plugin-basic-ssl 在安装 vite 的时候,遇到了版本解析错误,4.0.4_@types+node@17.0.45
  2. resolveDependency 函数中,会解析 @vitejs/plugin-basic-ssl 的 package.json。直接注意的是,它的 package.json 没有 dependencies 字段
  3. pkg 对象根据 package.json 生成,这一句代码中,由于 pkg.dependencies 不存在,因此会导致使用了锁文件的 dependencies 字段,这是不应该的,导致取了锁文件的 vite 版本号4.0.4_@types+node@17.0.45

1686401508470.png

既然知道了这个,我们就知道了这个错误出现的场景:

  1. 装了多个 Vite,有的 Vite 版本号是 4.0.4,有的是 4.0.4_@types+node@17.0.45 ,出现多个 Vite 的原因,是因为 peerDependencies,感兴趣可以查看官网的说明文档
  2. @vitejs/plugin-basic-ssl  的 dependencies 字段不存在(不是为空,是不存在)

只有同时满足以上条件才会报错,因此很多非 monorepo 仓库,都不会有这个问题,因为它们只装了一个 Vite。

当我知道了以上信息之后,我就可以提供一个最小的可复现 Demo 了

不过,我觉得既然都看到这里了,不如尝试一下自己修复

直觉告诉我,只要加一点代码就行了,判断 pkg.dependencies是否为空,为空就设置为 {}


if (!pkg.dependencies) {
    pkg.dependencies = {}
}

然后我把出错原因写到了 issue 中,顺便提了个 pull request 给开源作者,然后被告知需要补一下单元测试(这也的确是正常且稳妥的做法),至于后续单元测试怎么补,就不是本文该关心的问题了,以后有机会再聊。


总结


本文用我个人的例子,从发现问题,到调试代码,一步步地深入,直到最终找到问题。里面用到了很多调试相关的技巧,这些技巧可以帮助我们,即使在不熟悉源码的情况下,也能深入源码进行定位问题

这些技巧主要包括以下这些:

  • 全局搜索查找关键词/错误信息,找到相关的源码
  • 转化问题,将大的抽象问题,变小变具体
  • 在合理的位置打断点
  • 巧用条件断点,巧妙的设置断点条件
  • 利用函数调用栈

当然,仅仅有技巧也不行,你需要有解决问题的决心。那么,当你遇到问题时,你是选择避开它,还是选择解决它呢?

如果这篇文章对您有所帮助,可以点赞加收藏👍,您的鼓励是我创作路上的最大的动力。也可以关注我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)

目录
相关文章
|
4月前
|
安全 数据处理 开发者
Flutter相关痛点解决问题之iBox模块的源码结构的设计如何解决
Flutter相关痛点解决问题之iBox模块的源码结构的设计如何解决
|
安全 Shell 网络安全
5项目五:W1R3S-1(思路为主!)
5项目五:W1R3S-1(思路为主!)
78 0
|
前端开发 NoSQL 数据库
项目重点知识点详解
项目重点知识点详解
|
Rust Kubernetes 测试技术
Krustlet 入手案例
本文将对基于 Kind 部署 Krustlet 并实践 Demo 应用
417 0
html+css实战151-定位-子绝父相
html+css实战151-定位-子绝父相
296 0
html+css实战151-定位-子绝父相
|
存储 算法 Java
|
XML 数据采集 存储
接口自动化的关键思路和解决方案,本文全讲清楚了
与UI相比,接口一旦研发完成,通常变更或重构的频率和幅度相对较小。因此做接口自动化的性价比更高,通常运用于迭代版本上线前的回归测试中。 手工做接口测试,测试数据和参数都可以由测试人员手动填写和更新。
接口自动化的关键思路和解决方案,本文全讲清楚了

相关实验场景

更多
下一篇
DataWorks