关于 Shiny 调试你应该知道的

关于 Shiny 调试你应该知道的

12 min read

Finding your bug is a process of confirming the many things that you believe are true — until you find one which is not true.

—Norm Matloff

第一次接触网页开发是两三年前的事,那时我曾经问过计划带我入门前端的前辈:入门前端的标准是什么。他当时用一种极平和的语气和我说:学会 dubug。几年后的今天我即便也写过一点网页工具,但还是依然没能入门。反思一下:一是 JavaScript 学的不好,二就是不敢说自己有多少 debug 的能力。

遂放弃。

最近因为需要又要涉及一点网页工具开发,同时因为需求整体和 R 交互比较多于是决定用 R 的 Shiny 来搞一搞。

写了一个多星期我感觉 Shiny 确实解决了不熟悉前后端交互的人写网页的大多数问题,但如何 debug 的门槛还是摆在那里。比如前几天一个高手和我吐槽写 Shiny 时不知道改了什么突然不能正确运行了,更糟心的是还没有任何报错信息。当然,后来经过讨论发现其实并非没有报错信息,只是那时他没有找到而已。

这篇文章就结合最近学习的一点资料,大致聊聊在 Shiny 中 debug 的一些方法。

Shiny debug 主要有三个步骤,分别是调试 (Debugging),追踪(Tracing) 和错误处理(Error handling)

-Debugging: 所谓的调试就是猜,然后在你认为可能有错误的地方设置一个断点,再执行接下来每个语句的时候就都可以检查程序当时所在的状态。
-Tracing:可以在运行程序的时候收集程序运行产生的信息而无需暂停程序,在诊断系统性问题时因为我们无法频繁的中断使用这一方式就比较合适。
-Error handling: 在客户端和服务器端找到错误的来源并确定原因。

Debugging 调试

breakpoints 断点

说到「断点」,我不由的想对 bug 说一句:

静静地陪你走了好远好远,连眼睛红了都没有发现。

如果你知道哪一行的代码有错(这话本身就像 bug)或者猜测很可能是哪里不对,就可以直接在所在行设置一个断点。Rstudio 只需在行号左边点一下鼠标就会出现红点提示标记成功。开始运行 Shiny 程序后会在断点处停止执行,然后就可以开始逐步执​​行进行代码调试了。

如上图所示,我们在 40 和 41 行设置了两个断点,现在点击 Run App,代码运行会在 40 行处停下,绿色的箭头指向 41 行,并且在 console 中会有 browse 输出,如下图所示

这个时候我们可以方便的查看环境中已有的变量,例如这里已经运行完毕的 x 变量。如果想继续运行,可以点击 console 里 continue,程序又会向下运行。

现在环境中存在 x 和 bin 两个变量,同时在 console 的 browse[2]> 处你可以进行正常的 R 操作,例如查看变量内容或者做个加法运算,甚至你可以修改当前存在环境中的变量(当然不推荐这么操作)。

说到缺点,目前 breakpoints 只可以在 Rstudio IDE 中使用,而且只能用于 ShinyServer 函数中;另外如果代码很长显然不停的断点也很烦,同时它只能显示发生了什么但是无法告诉你为什么有些事情没有发生。

小结如下:

browser() 命令

其实从上面 console 的截图也可以看到,断点就是执行了一次类 browser() 操作。但和断点不同的是,browser() 本身可以被写到任何地方。写法如下图所示:

甚至你还可以把 browser() 写进判断语句中,只在某些特定的情况下执行。例如下图所示,在 input 中的 bins 小于 40 时候程序会正常的执行,当大于 40 时才会自动停下,然后你就可以在调试器中查看具体的变量等信息。

小结如下

Tracing 追踪

在许多情况下通过暂停执行来找问题比较困难,相反需要我们在程序运行时观察系统。对于 Shiny 的程序尤其如此,因为他不像 R 脚本那样线性运行。

Showcase Mode

Shiny 在启动时,runAPP() 有一个选项默认是读取配置文件中的设置,它就是 display.mode。如果我们设置为 display.mode="showcase",在 Showcase 模式下,代码将与应用程序一起显示,并且程序的 server 端代码在执行时会出现黄色的闪烁进行提醒。这一功能对于可视化哪些部分代码正在执行非常有用。

如果想要默认开启这一功能,可以在该 Shiny 目录下创建一个 DESCRIPTION 文件并写入如下内容:

DisplayMode: Showcase
Type: Shiny

小结如下

Reactive Log

在 Shiny 中经常会用到响应对象,当开启 Reactive Log 之后,程序运行时除了可以告诉你正在执行哪些响应之外,日志还可以帮助你可视化展示响应对象之间的依赖关系。在开启一个新的 R session 时首先配置 options(Shiny.reactlog=TRUE),然后在 Shiny app 中通过 Ctrl + F3 就可以启动可视化的 Reactive Log 文件,也可以在运行 Shiny 后使用 showReactLog 查看。详细信息可以在 官方文档 了解。

打印 tracing

在各种编程语言中,一个万变不离其宗的调试技巧就是不停的输出。在 PHP 里面是不停的 echo,在 R 里可以不停的 cat。使用 cat 可以帮助你在不终止程序的情况下查看变量值。在 Shiny 中,最好的方式是打印标准错误(stderr())。

# generate bins based on input$bins from ui.R
x    <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
cat(file=stderr(), "drawing histogram with", input$bins, "bins", "\n")

进行上述修改后,运行 Shiny 每次调整 input 都会在 console 中打印输出。如下图所示

小结如下

Shiny Server 进行 tracing

如果你的程序运行在 server 端而非本地,每次 Shiny 程序运行都会生成 log 文件,默认的路径是 /var/log/Shiny-server/*.log,而这一位置是可以在配置文件中通过 log_dir 进行修改的。如果程序运行没有出现问题,每次 Shiny 运行结束后文件会自动删除,如果报错了则会一直存在于 log 目录等你去宠幸。log 文件的命名格式为 <application directory name>-YYYMMDD-HHmmss-<port number or socket ID>.log

客户端和服务器端 Tracing

一个 Shiny 程序包括 client (浏览器) 和 server (R 进程) 两部分。这两者通过 websocket连接,websocket 接收来自客户端的状态,例如输入控件新的赋值,同时发布来自服务器端的状态更改,例如新的输出。在一些比较复杂的情况下,你可以通过打开 trace 来跟踪 JSON 格式的 websocket 内容。

如上图所示,在输出内容中,SEND 表示从浏览器发送到 R session 的数据;RECV 表示从 R session 发送到浏览器的数据。

这一部分目前还没有实际用到,理解到位了可以再写一些。

Errors 错误

跑程序最怕看到的就是报错,但是真要有问题了最希望看到的就是明确的报错。

R 报错

在 Shiny 中大多数报错信息都是由 R 引起的,在 0.13.0 之后的 Shiny 版本中已经有了比较直观的报错形式,会直接给出哪里的程序出现了错误。这里首先人为引入一个报错,当 input 大于 40 的时候停止程序并且抛出 too many bins 的报错信息。

运行程序后调整输入如果错误,可以观察 console 的输出内容:

首先直接观察颜色不同的部分,直接告诉我们 app.R43 行代码出现了问题。在报错部分,每一行内容前都有一个数字,例如 1,82,165 和 167,其表示的是在调用栈 (call stack) 中的索引,可以看到这个例子中有接近 170 个调用栈。

JavaScript errors

目前 Shiny 有很多第三方 JavaScript 组件,有时如果使用上面几种方式都没有定位到错误相关问题或者没有看到报错信息,很可能是 JavaScript 中发生错误导致程序出现了 bug。毕竟 Shiny 是个网页应用,各种和用户的交互少不了 JavaScript 的使用。

要进行 JavaScript 的调试在 Rstudio 就不灵了。如果你是通过 Rstudio 打开了一个单独 Shiny 页面,可以通过右键单击 Shiny 页面,选择 Inspect element 进入 JavaScript console;如果你的 Shiny 页面是open in browser,也就是在浏览器中打开的,可以直接通过 F12 进入开发者模式打开 JavaScript console 。

在 Shiny 中 UI 的每个部分都会有一个 id 参数,这个 id 对应的参数在浏览器中解析之后就是对应着 HTML 标签中的 id。在 HTML 中,这个 id 是必须唯一(区别于 name)。因此,在 Shiny 的 ui 中每一个 id 参数也必须唯一。解析效果如下图所示:

如果你不小心在 UI 中写入了两个一样的 id,在上图中就有两个标签的 id 都是 a,程序运行后在 Rstudio 并不会抛出什么错误,但是在 Shiny 页面端的各种操作就进行不了。如果不在开发者模式下进行调试只能通过各种方法在 Rstudio 进行测试,但是如果打开 JavaScript 的 console,就会看到其实已经给出了明确的报错信息。

当然,Chrome 开发者工具的用法是在太多,这也是我在文章开头提到的自己入门不了前端的原因之一。如果在你的 Shiny 中用到了大量 JavaScript 相关内容,或者需要定制很多 CSS 相关的内容,可以学习一下官方的开发者工具文档

至此,也就简单的写完了 R Shiny debug 的三个主要步骤,其中提到的每一个用法在实际使用中都需要进一步深入学习。当然,每一个方法用到的频率也各有不同,可以根据个人的实际情况进行后续的练习。

One more thing:shinyjs

写到这里本来文章就可以结束了,但是似乎总有哪里不对。

为什么在 Rstudio 的 console 里就不能查看 JavaScript 的 log 信息。要知道 Rstudio GUI 本身使用的就是 QT 框架,其中的很多部分都可以理解为一个网页。从维基百科或者它自己的说明中都可以看出这一点。

不信的话你也可以在 Rstuido 的每个 pane 里右击然后选择 Inspect element 看看会出现什么,比如在 Console 中右击

你会看到下面图所示的内容

既然如此,没有理由不去解决这个不方便的问题。其实在 R 中有一个专门为 Shiny 提高 JavaScript 使用体验开发的 R 包,叫做 shinyjs。这个包的存在让 Shiny 使用 JavaScript 变得强大和高效了很多。其中针对调试有两个专门的函数。

showLog

Print any JavaScript console.log() messages in the R console, to make it easier and quicker to debug apps without having to open the JavaScript console.

这个函数类似于 JavaScript 中的 console.log(),它可以把 JavaScript console 的信息显示在 R console 中而不需要再打开专门的 JavaScript console。

logjs

Print a message to the JavaScript console (mainly used for debugging purposes).

这个函数则可以把信息输出到 JavaScript console 中方便进行调试。

例如下面一段代码:

library(ShinyJavaScript)

if (interactive()) {
    library(Shiny)
    ShinyApp(
        ui = fluidPage(
            useShinyJavaScript(),  # Set up ShinyJavaScript
            actionButton("btn", "Click me")
        ),
        server = function(input, output) {
            observeEvent(input$btn, {
                # Change the following line for more examples
                logJavaScript(R.Version())
            })
        }
    )
}

运行后通过点击 button ,就可以把 R.Vsrsion() 的信息输出到 JavaScript console 中,如下图所示:

嗯,先写到这里吧。

我的学习材料


本文作者:思考问题的熊

版权声明:本博客所有文章除特别声明外,均采用 知识共享署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。

扫码即刻沟通
加入靠谱熊基地,和大家一起交流

前一篇

每周文献-190411-lncRNA功能exRNA分析以及DNA甲基化

后一篇

用漫威数据学习 ggplot2 facet