概述
本文是笔者的系列博文 《Bun技术评估》 中的第十六篇。
本文的主要内容,是探讨在bun的体系中,如何进行程序的调试工作(Debug)。
关于调试
"调试"这个中文名词其实是很技术化的,但我们看它的英文原型却很生活化,就是de-bug(去除-虫子)。因为这后面是有一个有趣的典故。
那是在1947年在哈佛大学,海军军官/计算机科学先驱Grace Hopper(葛丽丝·霍普)中校负责领导使用和维护一台MarkII型继电器计算机。当时工程师们发现计算机突然出现异常,无法得到预期的运算结果,于是打开机器内部进行检查,结果真的发现了一只飞蛾卡在继电器里,把电路短路了!
于是他们小心翼翼地把这只飞蛾从继电器里取出来,并贴在实验日志上,旁边写上了一句(借用下图):
First actual case of bug being found.
第一次发现真正的「虫子」造成的问题。)

虽然第一个bug确实是一个实体+硬件故障,但从此以后,在信息技术特别是软件技术领域,"bug"就有了一个新的意义,就是程序中的缺陷和错误。这个错误可以是有很多种形式,既可以是硬件失效造成的故障,也可以是由于逻辑冲突、计算越界、内存引用不当、或者数据类型错误等等软件方面的问题,只要能够导致程序无法按照预期正常执行并且得到一个正确的结果,都可以统称为bug,而这个排除错误的过程,也被称为debug。
现在的系统和软件技术越来越复杂,debug的工作也变得越来重要,因为一个潜在的软件缺陷,可能会造成严重的后果。所以调试技术也逐步发展成为软件开发技术和工程中的一个非常重要的组成部分,因为它直接影响软件开发工作的效率、质量和后期运行的性能和稳定性。
所以,任何一个成熟的开发工具和体系,都应当支持某种程度的调试方式。从广义上笔者觉得可以认为,任何能够帮助开发者按照自己的需要,能够获得程序执行过程中的状态和信息的技术手段,都可以称之为调试。
但在实现方式上,通常有执行日志的形式和可以通过设置断点和控制程序执行的步骤的方式。狭义上的调试,笔者认为应当是后者。我们先来看看日志的方式。
调试日志
所有的开发者,在入门的时候,写的第一行可以运行的程序,大概率就是那个"hello world!"。不同的语言,可能有不同的实现方式和细节,但其实基础原理都是一样的,就是向"标准输出(stdout)",打印一段信息,来标识在当前的开发和执行环境中,这个程序可以是正常执行的。
这一功能,当然也可以用于在代码中,选择合适的位置,当程序运行到这里的时候,向外部输出一些信息,内容可以自己定义,比如变量的状态或者当前的值,这一,开发者就可以有限的了解当前程序运行的情况了。特别是程序运行有问题或者出错误的时候,开发者会根据出错的情况,分析可能会出问题的代码,并在其前面编写打印一些调试信息,然后在运行检查这个状态,来帮助故障的排查,这就是一种初级的调试技术。这种编写代码输出调试信息来辅助运行错误排查的技术,通常被称为log(日志)调试。
虽然这个方式简单方便,并且通用性很好,但其实在成熟的开发体系中,并不建议使用。因为首先这种方式会侵入代码,维护和清理都不太方便;而且这种方式一般不区分开发测试环境和生产部署环境,在生产环境中运行,会影响运行的性能,如果在部署前清理,也要额外的工作。如果确实要使用调试日志,更建议的方式,要么引入更专业的日志系统(它们会区分执行环境),要么使用开发工具和系统带有的调试日志模式,我们后面会有相关的内容。
有一些读者可能认为,很多业务系统本身也有运行日志记录的业务需求,可以使用这种方式来实现。但实际上也不建议直接使用系统级别的信息输出功能,更建议使用专业的日志程序模块,来实现这个业务需求。因为相对而言,它们能提供更丰富的日志级别、记录、格式、集成和管理功能。这其实是另外一个系统和问题,这里不展开探讨。
让我们还是回到程序中的运行日志,在bun程序中,输出日志信息,一般就是使用标准的console对象,还支持标准的日志级别,非常简单:
js
// 普通信息
console.log("Value of i: ", i);
// 警告信息
console.warn("Warning Mem High: ", process.memoryUsage());
// 错误信息
console.error("Some Thing Wrong: ", err.message);
console的使用,在JS系统的开发语言和工具中,基本上都是标准化的。笔者已经在以前的博文中进行了探讨(当时基于nodejs系统),所以这里不再赘述。
从上面的讨论我们也能够发现,虽然调试日志可以在某种程度上赋予了我们来了解程序执行时状态的能力,但这种方式还是让人感觉有点不够直观,只能从一个外部输出的内容来反推程序的执行情况,而且使用也不太方便,需要频繁修改原始代码来确定要调试的步骤。
为此,一个更加高级有效的调试方法论被提出来,并作为所有成熟的语言和开发系统都基本上都实现和配置了调试技术,就是所谓的"断点-步进"调试系统。而且它被默认为就是标准的调试系统。
断点-步进调试系统
笔者认为,所谓"断点-步进"调试系统,就是基本上可以以以下的方式工作的调试系统或者环境:
-
原始代码应当以文本行的方式组织,这是为了方便开发者的观察和理解
-
开发者可将某些代码行标记成为"断点(BreakPoint)"
-
开发者可以使用"调试模式(Debug Mode)"来执行程序
-
程序以调试模式执行的时候,当执行到断点标记的代码行时,会暂停继续执行,并提示开发者程序已暂停,并指示当前暂停的位置(代码行)
-
此时开发者可以以某种方式,来观察执行的状态,如获取暂停时某些变量的当前的值,甚至可以尝试在当前环境下执行某些操作,如调用函数,或者设置变量的值
-
开发者可以选择恢复程序运行,然后程序可以继续运行,直到下一个断点或者程序结束
-
开发在暂停状态下,也可以选择步进执行,即执行代码中的下一步指令,通常在代码显示中是下一行,并且观察程序执行的状态和响应
-
如果遇到函数调用,在执行逻辑中,步进可以追溯到其调用的函数代码行中,包括返回到主调用程序
-
开发者可以随时选择停止程序或者重启程序,以方便修改后的快速重新执行
和日志式的调试技术相比,这一调试技术显然更加先进灵活。它赋予了开发者一种从内部来观察程序执行的角度和体验,让开发者能够更加直观方便的理解程序执行的过程和问题,特别对于错误排查工作,这是非常有效和高效的,因为开发者可以一步一步的看到错误是如何发生的,并且了解到错误是如何具体影响到程序的执行的。
前面展示的调试流程,只是标准和基本的调试系统应当提供的基本功能,有的调试系统还可以提供更多的调试方法、配置和技术,从而提供给开发者更方便和灵活的选择,从而极大的提高开发和调试效率。
VSCODE基本调试流程
下面,笔者想要以VSCODE IDE和一个简单的Bun程序为例,来说明一下程序执行和调试的一般过程:
- Bun支持
VSCODE在设计上,就是一个中立的IDE环境。理论上它可以用于任意编程语言的应用开发。所以,其实,它对于某种编程语言的支持,是通过一些内置或者外置的模块来完成的。例如,nodejs在VSCODE里面,应该就是一个一等公民,是内置支持的。而bun作为一个非主流的工具,正常确实也是没有内置支持的。所以我们可能需要通过扩展市场来进行扩展。
打开VsCode的扩展面板,然后搜索bun(下图),应该可以找到相关的扩展。比如这里就有一个"Bun for Visual Studio Code",名字非常正规,下载数量和星级评价也不错,笔者就选择了这个扩展。点击"安装"按钮,就可以将这个扩展安装到了VSCODE中,让其具备了支持bun应用开发的能力。

一般每种开发语言和环境,在VSCODE中都有类似的扩展、,比如PHP、Java等等。但要进一步确定能够满足开发者对于执行调试的需求,最好还是阅读一下它的发布说明。比如这里就明确的提到了"Debugger Support",我们基本可以确认这个扩展是带有调试支持的功能的。
- 项目设置
其实,在VDCODE中,进行调试,不需要特别设置项目和文件。理论上任何一个文件,只要它自己能够被某种方式执行,就可以被调试。例如下面有一个笔者用于展示的测试项目,笔者在这里简单编辑了一个ts文件,并且可以使用bun来执行(内容不重要,只是为了展示流程):

注意,这里用户可以将侧边工具集,切换到运行调试面板(RUN AND DEBUG)。这里还有几个其他选项,我们可以在后面进行了解。
- 断点设置
在代码窗口里面,最左边那一栏可以用于设置断点,用鼠标点一下,就可以将当前行设置为断点(红色标识),在点一下,就可以取消这个断点。所谓断点,就是说如果程序有机会运行到其所在的哪行代码,程序就会暂停,看起来像是"中断"了一样。这时候,开发者,就可以有机会做一些后续的调试工作。关于这些工作的内容,我们会在后面展开讨论。

显然,在一个程序中,我们可以按照需要设置多个断点,来重点监控我们觉得容易出问题或者需要关心的代码段。
- 执行和调试
这里为了展示VSCODE调试的简单和灵活性,没有设置任何调试项目。而是直接点击在第一步中,运行调试面板中的"Run And Debug"按钮,这时系统会尝试执行并且调试当前打开的这个文件(index.ts)。由于前期没有任何设置,这时VSCODE会显示一个菜单,尝试让用户选择一个调试器(Select Debugger),这里可以看到刚好有一个bun可以选择。选择这个Bun,程序就会执行,当运行到前面设置断点的那行代码的时候,程序就会暂停(图)。

在这个时候,程序员就有机会来检查代码执行的状态,例如查看这个时候,某个变量的值是否按照预期进行了设置。在VSCODE界面中,将鼠标移到变量位置,就可以完成这个工作。当然也可以使用Debug Console(调试控制台)来执行这个任务。
- Debug Console 调试控制台
在VSCODE启动调试的时候,一般会同时启动一个调试控制台(Debug Console)。在这里开发者可以有机会和程序进行某种形式的交互。最常见任务就是查询某个变量当前的值(下图):

这里从调试控制台中我们可以看到,首先调试器已经正常的挂载了,然后可以在命令行中,用户可以输入变量的名词并回车,调试控制台可以展示当前的值。但是要注意,这个查询是一次性的,并不会动态变化(浏览器的调试控制台好像是动态的)。这里的命令行输入功能其实挺强大的,不仅可以查询变量的值,还可以输入表达式,甚至进行函数的调用,非常像一个REPL环境,当然它有个优势就是执行上下文是当前程序的执行环境和状态,就跟开发者可以在那个环境可以手动的执行程序一样。
此外,调试控制台在调试的时候,还可以作为标准输出的区域(注意调试面板显示的log信息),就和从命令行环境执行程序时,一般是输入到命令行环境一样。
此外,左边的调试面板在启动调试的时候,也会发生一些变化,会出现几个方面的内容。也有Variables变量区域,可以看到当前变量的值。其他的如Global,Watch,CallStack,LoadedScript,BreakPoints等,我们会在后续的内容中进行讨论和解释。
- 恢复运行
如前所述,断点的功能是将程序执行暂时中断,然后让开发者可以查看当前程序运行的状态。查看完成之后,开发者就可以选择恢复程序运行。恢复执行可以是很简单,也可以有很多选项和方式。注意到程序暂停时,窗口右上角那一排方形的控制按钮了吗?这就是调试控制工具栏。开发者可以使用这工具栏来控制程序调试的步进和恢复。当然,VSCODE提供的操作选项是很多的,提供了极大的灵活性。

我们从左到右简单说明。
最左边常用的就是恢复执行,这时程序会恢复执行,直到结束或者遇到另外一个断点。
然后是"下一步",就是简单的理解成为代码的下一行(其实是下一个执行的代码),这时开发者可以一步一步的执行代码,来观察代码执行的效果和变量对象状态的变化。
第三个按钮是"进入",这个操作用于代码执行到函数调用的时候,进一步跟踪到所执行的函数中。而前面那个"下一步"会将函数调用作为一个简单的步骤执行,直接完成,而不会进入所调用的函数。
第四个按钮,其实是和第三个配套使用的,是从所调用的函数,返回上一层。
第五个按钮,就是重启调试过程,这个操作通常在发现问题,修改代码后,重启程序和调试。
第六个按钮,就是简单的停止程序。还有一个选项是断开调试器。因为调试程序和程序执行本体在逻辑上是分离的
上面就是VSCODE进行程序调试操作的基本过程。由于几乎相同的原理和方法论,如果读者使用其他的开发工具,比如IntelliJ或者Eclipse,应该也是类似的过程,不会有太大的差别。
断点的选项
断点的设置,其实是有很多选项的。简单的断点,就是所设置的代码所在的位置。当程序运行到这里的时候,就会暂停进入调试状态。但是,如果我们在断点区域点击右键,就会出现一个菜单(下图),这里面我们可以看到更多的断点类型:

-
断点(BreakPorint),就是前面提到的普通简单断点
-
条件断点(Condition Breakpoint)
就是开发者可以设置断点激活的条件。它也有很多选项,如可以设置一个条件表达式(比如 i == 2)。

这个特性比较有用,比如在一个比较大的循环,开发者希望观察运行要结束的时候,程序的状态,就不需要手动控制程序执行,直接设置一个条件断点就可以了。
- 日志断点(Log Breakpoint)
其实这个特性,笔者觉得叫断点日志,恐怕更合适。因为这种断点本质上,在运行是不会中断程序,而是在执行到所在代码行的时候,可以打印日志信息。这个特性其实非常有用,笔者觉得完全可以在开发阶段替代普通的调试性log功能。设置内容的方式也很简单,就是直接输入日志信息,并使用 {} 来标记变量,比如 " i value is {i} "。
- 计数断点(Hit Count)
这个断点不是简单的断点。特别是在一个循环体,或者多次调用过程中,开发者可以设置一个计数器。每次程序运行到这一行代码,都会触发计数,当计数器的值达到设定值时,程序将会暂停进入调试状态。比如如果开发者只关心循环或者函数的调用到最后一次的情况下,或者方便后续步进执行,就可以设置这种断点。
- 等待断点(Wait For Breakpoint)
这个类型笔者并没有觉得很好的理解了。从使用方式上来看。它可以设置所谓"断点"的"断点"。因为在这种模式下,需要选择另外一个断点,猜想就是前一个断点的触发,是后一个断点的触发条件。但笔者的应用场景中,很少遇到这种情况。所以没有太大的机会来验证和实践。
笔者这里还发现一个有趣的设置,就是在同一个代码行里面,可以设置多种类型的断点,例如可以同时设置条件断点、日志断点和计数断点。而且,在VSCODE中,不同的断点,虽然大体上都是一个红色的标记,但还是有一些细节上的差异,方便开发者快速分辨断点的类型。
如果当前的代码行,已经设置了断点,点击右键,则会出现一个有点不一样的菜单(下图):

-
移除断点
-
禁用断点: 就是暂时禁用,但不删除断点(可能后面还会打开)。
-
编辑断点,可以作为条件断点进行编辑
-
运行到当前行: 在调试过程中,运行到当前行(只能在调试时使用)
最后,在调试的面板中,有一个"断点"区块(下图):

在这里我们还可以设置一些和断点相关的选项:
- 所有异常(All Exceptions)
这个选项将所有异常都作为自动化的断点。当遇到异常抛出的时候,就暂停程序,方便开发者查看当前状态。
- 未捕获的异常 (UncaughtExceptions)
一般应该在开发阶段,把这个选项打开,不然如果没有很好的规划try-catch选项时,程序会直接崩溃退出,开发者就没有机会了解退出的状态和原因。下面是一个简单的示例:

- 调试器语句(Debugger Statements)
可以看到默认是开启的,就是打开调试断点触发的状态。关闭这个可以在全局禁用所有设置好的断点。
- 断言失败(Assertion Failures)
这个可能是用于测试断言的语句。当断言操作失败时,触发断点。应当只在断言所在程序行有效,所以通常用于测试程序当中。
- Mircotasks
应该指的是打开或者关闭在微任务中的断点。笔者不是特别确定,也没有更多的信息。
- 代码文件断点
就是程序文件中的断点。会列出所有当前项目中,设置的断点和所在代码行的文件。在这里开发者可以统一的管理所有设置的断点。
launch.json文件
前面我们展示的是VSCODE使用默认的方式和配置来执行一个调试启动的方式。但通常情况下,开发者需要对这一过程进行一些配置。VSCODE使用的就是.vscode/launch.json这个文件。如果我们在初始的调试面板中选择"create a launch.json file",然后选择"bun"选择,就可以创建一个包括bun启动项目的launch.json文件。默认的文件内容如下:
launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"noDebug": true,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/",
"stopOnEntry": false
}
]
}
然后我们就可以在调试面板中看到这些启动选项了,用户可以注意这些项目名词和launch.json文件中项目的对应关系:

这里面一次性创建了三个启动项目:
- Debug File(调试)
就是以调试模式启动程序。执行后,遇到各种断点,就会在当前代码行上暂停程序。这里的File应当是只当前IDE打开的程序文件,运行前要注意选择入口文件。
- Run File(运行)
就是以普通的执行模式启动程序。这时就是一个正常执行的程序,断点设置是无效的。(因为noDebug的设置)
- Attach Run(附加)
这种模式启动后,可以使用外部调试工具,通过ws协议来连接执行程序并进行调试。虽然大致明白其工作原理,但笔者没有在自己的环境中实现实际的操作。只是通过另一种配置方式,开启了外部基于浏览器的调试工作(原理和过程应该类似)。
步骤是:
- 使用调试模式执行程序
shell
bun --inspect-brk index.ts
--------------------- Bun Inspector ---------------------
Listening:
ws://localhost:6499/twuev52agmq
Inspect in browser:
https://debug.bun.sh/#localhost:6499/twuev52agmq
--------------------- Bun Inspector ---------------------
- 在浏览器中打开所展示的地址:

这些步骤,可以完全脱离VSCODE,就是标准的调试协议(基于WS协议)。虽然表面上网页的地址是debug.bun.sh,但实际上它只提供了一个UI和调试操作框架,实际的数据和内容,来自本地的应用执行。虽然在有完善的IDE工具情况下,笔者想不出来在开发过程中有什么理由这样做,但笔者想到一个可能的可以远程执行和调试的场景,就是临时性的生产环境的调试。可以在生产环境中,临时的使用--inspect标记执行代码,然后在本地通过ssh port隧道将调试端口映射到本地,最后在浏览器中打开调试界面,就可以观察到生产系统实际运行的状态了(没有必要不建议这么操作,因为很容易影响生产环境代码的执行和用户使用)。
当然,如果开发者对launch.json的格式比较熟悉的话,也可以直接自行编辑和增加相关的执行和调试项目,也可以根据需要编辑这些执行项目的配置信息,来满足开发调试的工作需求。
其他设定和选项
在VCDOE中,还有一些和调试相关的设置和项目,这里简单列举一下。
JS Terminal
在初始模式下,控制面板还提供了一个"JS Terminal"的按钮,点击后会打开一个名为"JavaScript Debug Terminal"的终端窗口。

其实它的作用就是一个特殊的终端,在里面启动的应用程序,可以自动附加调试器,这时后和从调试面板选择调试项目启动,就没有什么区别了。
Debug URL
在初始的调试面板中,还有一个Debug URL按钮,但这个实际上没有什么实际作用。点击后VSCODE会提示你输入一个http地址,然后它会打开浏览器定向领导这个地址,应该就是使用浏览器作为调试工具来使用了。
Global
在调试面板的"变量"区域中,除了Locals(本地变量)之外,还有一个有趣的项目就是Global全局变量(下图),从内容上来看,其实就是当前系统所使用的"根对象",包括了常用的方法,常数等等内容,笔者会在另外一篇博文中专门讨论这个问题。

Watch
此处,开发者可以自行定义想要特别跟踪的某些变量,它会在这里显示代码执行过程中,当前暂停时,这个变量的值。这里还可以使用表达式,应该也支持函数调用。如示例中就使用了一个表达式,然后观察表达式计算的结果。
Call Stack
这个区域显示当前断点或者运行到的代码行的调用堆栈,最下面那一行应当就是程序入口,然后是依次的调用关系,直到最上面当前执行暂停的代码行。从这里开发者可以清晰的了解到调用执行的过程,特别是复杂调用之间的关联关系。
Loaded Scripts
这个区域显示,在当前的运行状态(暂停时)下,相关联和加载的脚本文件的列表。从这里开发者可以了解文件之间的加载关系。
小结
本文探讨了bun系统中程序调试相关的问题。先阐述了程序调试的基本概念和方法,然后结合VSCODE这个开发工具,讨论了在开发过程中的一般调试的过程和操作,和其他相关选项和信息。