Emscripten Fetch 接口的一个潜在内存泄漏问题
近日发现了一个非常刁钻的可能引起基于 Emscripten 编译的 WASM 程序内存泄漏的问题。Emscripten 工具链提供了 Fetch 功能模块,这个模块允许我们调用浏览器的 fetch 接口来进行网络访问。
一个使用 fetch 接口的简单例子是:
1 | #include <stdio.h> |
Fetch API 提供了一些比较高阶的功能,一种一个比较重要的功能是,他可以将下载的内容缓存到 IndexDB 中,这个缓存机制能够突破浏览器自身的缓存大小的限制(一般超过 50MB 的文件浏览器的自动缓存机制会拒绝缓存)。但是这个缓存机制会导致内存泄漏。
1 泄漏产生的过程
在开头的例子中,我们需要再 onerror 和 onsuccess 回调中调用 emscripten_fetch_close
接口来关闭 fetch
指针代表的请求。在关闭过程中,fetch 使用的数据缓存区将会被回收。这个过程如下:
1 | EMSCRIPTEN_RESULT emscripten_fetch_close(emscripten_fetch_t* fetch) { |
可以看到,回收并非总会发生, emscripten_fetch_close
函数会对 fetch 的部分状态进行检查,如果检查失败,则会返回一个 EMSCRIPTEN_RESULT_INVALID_PARAM
的错误码,并且不会执行后续的清理过程(`fetch_free
)。被检查的两属性中,fetch->id
是我们需要关注的对象。fetch->id
这个属性作为 fetch
的唯一标识符,是用来建立起 C++ 端的请求对象和 JS 端的请求对象的映射的。id 的值在 JS 端分配。查看源码中的 Fetch.js
文件,
1 | function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { |
这是唯一的一处 id 复制。这段代码位于 fetchXHR
函数中,这意味着只有发起了 XHR
请求时,id 才会被分配。那么,如果缓存存在呢?这时不会调用 fetchXHR
函数(而是调用 fetchLoadCachedData
函数)。这意味着回调函数中我们试图调用 emscripten_fetch_close
函数来关闭请求并回收资源时,这个回收过程无法进行,这导致了内存泄漏。
2 怎么解决这个问题
要解决这个问题我们只需要强行让 fetch->id == 0
的检查无法通过即可,我们可以在 emscripten_fetch_close
调用前,强行设置 fetch->id
为一个非零值。那么什么值合适呢?如果我们取值和已有的请求的 id 相同,那么 emscripten_fetch_close
可能将那个请求关闭。研究 id 分配的过程(即 Fetch.xhrs.allocate
的实现)
1 | // libcore.js |
可以看到,id 是顺序分配的,且使用过的 id 会被回收使用(freelist
)。因此我们可以设置一个较大的值,只要同一时间最大的并发请求数量不超过这个值,那就是安全的。我一般选择设置为 0xffff
。 那么,正确的关闭请求的方式是:
1 | if (fetch->id == 0) { |