cenfun / monocart-coverage-reports

A code coverage tool to generate native V8 reports or Istanbul reports.
MIT License
75 stars 6 forks source link

[Help] 跳转或者刷新都会导致当前页面覆盖率数据丢失 #88

Open fanksy opened 5 days ago

fanksy commented 5 days ago

这个问题真的很无解,v8收集覆盖率总会丢失一部分数据

当发生页面导航时,Chrome 会: 销毁当前页面的 JavaScript 执行上下文 清理相关的内存数据 创建新的执行上下文 重新初始化 V8 引擎的状态 CDP (Chrome DevTools Protocol) 的覆盖率收集是基于 V8 引擎的 profiler 功能实现的。当执行上下文被销毁时,这些 profiler 数据也会被清理。 在 Chromium 的实现中,这个行为是有意为之的,因为: 保证了内存不会泄漏 确保了每个页面都有独立的执行环境 避免了跨页面的状态污染

试图通过拦截导航请求进行数据收集,却被阻塞了 请求拦截发生在网络栈 覆盖率收集需要访问 V8 引擎 两者都需要主线程参与 Chrome 为了避免竞态条件,会阻塞后续操作 调用栈大致如下: Network Stack → Fetch.requestPaused → CDP Command (Coverage.takePreciseCoverage) → V8 Engine (blocked) → Main Thread (waiting) 这就解释了为什么在请求拦截期间尝试收集覆盖率数据会被阻塞 - 它们共享了同一个执行上下文,而 Chrome 的安全机制阻止了这种潜在的竞态条件。

cenfun commented 5 days ago

拦截导航请求是·怎么做的? 或者试试这执行上下文关闭时收集?https://playwright.dev/docs/api/class-browsercontext#browser-context-event-close 同理页面跳转或关闭时有没有机会收集?

退一步减少丢失,可以用定时收集?比如1分钟收集一次

我不知道具体你的需求是什么,感觉是在用人工测试(随意点击)的时候收集? 否则如果是自己的测试case,你永远知道何时页面会跳转,此时就可以触发覆盖率收集

但目前MCR使用覆盖率的接口,默认就是开启了resetOnNavigation: false,不重置覆盖率在页面导航的时候

page.coverage.startJSCoverage({
    resetOnNavigation: false
}),
page.coverage.startCSSCoverage({
    resetOnNavigation: false
})

https://playwright.dev/docs/api/class-coverage#coverage-start-js-coverage

最好有个重现问题的代码可以让我在本地重现,以帮助更快的解决问题

fanksy commented 5 days ago

playwright 可以用page.route("*/", route => {})来拦截, CDP自身也提供了拦截方式,但是这样拦截就会导致v8主线程阻塞,获取覆盖率会导致死锁

page.route("*/", route => { cdpsession.send(“profiler.takePreciseCoverage”) })

resetOnNavigation这个参数涉及的源码我也看了,就是用来清理之前收集的css或者js的源码信息,与覆盖率数据无关

“退一步减少丢失,可以用定时收集?比如1分钟收集一次” 现在就是定时收集的,跳转刷新时间很短,基本上都会丢失location.href前执行的代码覆盖率

即便你是自动化测试,你也无法预测代码里什么时候会跳转,只要跳转刷新就会重置上下文,覆盖率就会丢失,对于istanbul也有这样的问题,现在尝试在页面里注入拦截代码,看看能否拦截跳转刷新

这个很好重现,写个location.href跳转,在前面加点代码,这段代码执行前收集一次,跳转刷新后再收集一次,就会发现没有收集到location.href前面执行的那段代码的覆盖率

具体可以看这个:https://issues.chromium.org/issues/378692744

cenfun commented 4 days ago

resetOnNavigation这个参数涉及的源码我也看了,就是用来清理之前收集的css或者js的源码信息,与覆盖率数据无关

这个倒是没想到,看了下代码,好像确实只是不清理旧的文件,相同的文件的覆盖率直接就替换了,并没有合并,导致之前的覆盖率数据丢失

MCR也实现了一套接口来收集v8覆盖数据: https://github.com/cenfun/monocart-coverage-reports/blob/main/lib/client/coverage-client.js#L84 理论上可以实现新的页面如果有相同的文件,可以和老的覆盖率进行合并,参考playwright即可 https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/chromium/crCoverage.ts

但是这需要在跳转之前,调用 Profiler.takePreciseCoverage收集上一次的覆盖率,所以问题回到chromium本身你提到的问题 如果是这样,看起来node端是无解的

cenfun commented 4 days ago

新的思路,在页面做拦截(部分应该可以实现)

<body>
    <a id="myLink" href="https://www.example.com">Click me (preventDefault)</a>

   <script>
        // 获取 a 标签元素
        const link = document.getElementById('myLink');

        // 为 a 标签添加 click 事件监听器
        const handler = async function(event) {

            // 在事件处理函数中调用 preventDefault() 方法,阻止跳转
            event.preventDefault();

            // trigger一个事件或者什么信号,应该有办法通知playwright收集覆盖 
            await collectCoverage();

            // 收集完再重新跳转
            link.removeEventListener('click', handler);
            link.click();
        };
        link.addEventListener('click', handler);
    </script>
</body>
fanksy commented 4 days ago

试过了,点击确实可以拦截,但主要是location.href这样的自动跳转,基本做不到

尝试过插件的方案,v3新的声明式拦截功能太弱,用策略方式安装插件虽然可以用v2版的webrequestblocking同步拦截功能,但局限性太多,不能用xhr同步调用,里面只能写fetch异步远程调用,这跟没拦截一样,chrome官方完全将插件限制到可有可无的状态了。很多用istanbul就是通过插件方案收集的,里面大概率也会拦截跳转刷新,同样也会碰到上面的问题,他们用的都是v2插件,但v2插件在今年都被禁用了,他们的覆盖率收集应该都有问题。

这个问题只能等chrome开发人员对issue的处理回复了