likes
comments
collection
share

一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct

作者站长头像
站长
· 阅读数 23

Bug缘起

我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ctrl+v,ctrl+s保存一气呵成,正查看着效果,突然感受到手中的二手MacBook发烫了起来,风扇也开始呼呼的转,我之前一度以为这电脑是没风扇的,直到那天,才第一次感受到了风扇。于是我打开柠檬管家,好家伙,温度直达90°C,而我开发的网页也越来越卡,后面直接崩了!

一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct 一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct

Bug分析

当时发现这个Bug都惊呆了,这么严重,怎么线上没人反馈。赶紧联系测试,让他们也试试,结果他们没人复现,那估计就是我本地开发问题了。但是我后面也没办法百分百复现,没搞懂出现的逻辑。

是否中挖矿病毒?

之前公司服务器遇到过挖矿病毒,一般挖矿病毒是一直运行的,我这个只是开发的时候运行,而且查看任务管理器没发现奇怪进程,排除掉。

一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct

内存泄露排查

在网上看了下如何排查内存泄露,打开Memory,Performance等面板观察,结果因为出现的时候太卡,直接卡崩了,也没办法对比了。

一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct

灵感转机

当时想到会不会是第三方插件引起的,使用排查法,一个一个排查浏览器插件,最后发现是它:vue.js devtools。后面处理的办法也很简单,直接不用它就完事了,用beta版的暂时解决了,但是对于BUG出现的原因以及解决办法还是没有找到。

无心插柳

最近工作不忙,又想起这事来,于是准备再碰碰运气。在开发者工具使用以下代码捕获了message事件,发现控制台一直在输出,ok,那看来引起的原因就是它了。

window.addEventListener('message', e => console.log(e))

一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct 展开Event对象,发现是它干的。

    source: 'vue-devtools-backend-injection',
    payload: 'listening'

代码追踪

直接找到vue-devtools的仓库,克隆下来搜索关键字,找到如下关键代码。以下代码初看很简洁,细看有点懵。先监听了一个message,后面又触发,触发的回调还是触发事件,这不就直接死循环了嘛。后面问了下GPT,哦,它其实在满足某个条件的时候,会移除监听,所以正常不会死循环。

// backend.js
window.addEventListener('message', handshake)

function sendListening() {
  window.postMessage({
    source: 'vue-devtools-backend-injection',
    payload: 'listening',
  }, '*')
}
sendListening()

function handshake(e) {
  if (e.data.source === 'vue-devtools-proxy' && e.data.payload === 'init') {
    window.removeEventListener('message', handshake)

    let listeners = []
    const bridge = new Bridge({
      listen(fn) {
        const listener = (evt) => {
          if (evt.data.source === 'vue-devtools-proxy' && evt.data.payload) {
            fn(evt.data.payload)
          }
        }
        window.addEventListener('message', listener)
        listeners.push(listener)
      },
      send(data) {
        // if (process.env.NODE_ENV !== 'production') {
        //   console.log('[chrome] backend -> devtools', data)
        // }

        window.postMessage({
          source: 'vue-devtools-backend',
          payload: data,
        }, '*')
      },
    })

    bridge.on('shutdown', () => {
      listeners.forEach((l) => {
        window.removeEventListener('message', l)
      })
      listeners = []

      window.addEventListener('message', handshake) // 关键是它
    })

    initBackend(bridge)
  }
  else {
    sendListening()
  }
}

移除监听的地方

在proxy.js这里会监听到上面触发的事件,然后通知上面init初始化,移除掉handshake的监听。

// proxy.js
window.addEventListener('message', sendMessageToDevtools)

function sendMessageToDevtools(e) {
  if (e.data && e.data.source === 'vue-devtools-backend') {
    port.postMessage(e.data.payload)
  }
  else if (e.data && e.data.source === 'vue-devtools-backend-injection') {
    if (e.data.payload === 'listening') {
      sendMessageToBackend('init')
    }
  }
}

function sendMessageToBackend(payload) {
  window.postMessage({
    source: 'vue-devtools-proxy',
    payload,
  }, '*')
}

初见端倪

通过不断加debug发现,把开发工具关闭后,会触发onDisconnect,然后proxy.js会移除上面的事件监听。backend会进入bridge的‘shutdown’的回调,也就是这是这里把前面初始化时已经移除的监听又给加上了,导致后面出现触发message事件时,死循环了。

总结复现步骤

  1. 准备好一个vue页面,会定时更新组件的或者更新组件树的,比如轮播图页面。
  2. 打开vue开发者工具。
  3. 打开浏览器的任务管理器,cpu倒序展示。
  4. 最后关闭vue开发者工具。
  5. cpu上升到100%。

我的环境

- MacBook Pro (13-inch, M1, 2020)
- macOS Monterey 12.5.1
- Chrome 126.0.6478.185 (arm64)。
- 火狐要手动触发更新事件,也能复现。

复现代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  <title>test</title>
  <style>
    .el-carousel__item h3 {
      color: #475669;
      font-size: 18px;
      opacity: 0.75;
      line-height: 300px;
      margin: 0;
    }
    
    .el-carousel__item:nth-child(2n) {
      background-color: #99a9bf;
    }
    
    .el-carousel__item:nth-child(2n+1) {
      background-color: #d3dce6;
    }
  </style>
</head>
<body>
  <div id="app">
    <el-carousel indicator-position="outside">
      <el-carousel-item v-for="item in 4" :key="item">
        <h3>{{ item }}</h3>
      </el-carousel-item>
    </el-carousel>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  <script type="text/javascript">
    new Vue({
      el: '#app'
    })
  </script>
</body>
</html>

后续排查

  • 在Git上排查加那行代码的原因,发现是2年前加上的,修复自动重连的BUG。 Commit。真想直接把那行代码干掉,搞得我的M1芯片红温了好几次。

  • 本来准备提个ISSUE,结果发现已经有很多老哥提过了,并且还有个老哥3月提出了PR,但是没人理,难搞。下次开发的话,不要关闭devtools就不会出现了这个问题了。

转载自:https://juejin.cn/post/7402204094449188915
评论
请登录