使用 webassembly 在浏览器中查询大型数据集

Querying a large dataset in-browser using webassembly

为了论证,假设浏览器允许在 WebAssembly 应用程序中使用 4GB of memory。忽略压缩和其他数据存储考虑因素,如果用户有一个 3GB 的本地 csv 文件,我们可以使用 webassembly(当然是 javascript)完全在内存中查询该数据。例如,如果用户的数据格式如下:

ID Country Amount
1 US 12
2 GB 11
3 DE 7

然后在几行代码中我们可以做一个基本算法来过滤到 ID=2,即 SQL 相当于 SELECT * FROM table WHERE id=2.

现在,我的问题是在 任何 浏览器中是否有可能(并且可能带有实验性标志 and/or 选择了某些用户首选项)以便可以在即使压缩得当,也放不下内存的文件。例如,in this blog post,一个 ~500GB 的文件被加载然后被查询。我知道 500GB 的数据并没有完全加载到内存中,并且可能有一个面向列的数据结构,因此只需要读取某些列,但无论哪种方式 OS 都可以访问文件系统,所以可以使用比可用内存大得多的文件。

是否可以在 WebAssembly 浏览器应用程序中以任何方式执行此操作?如果是这样,如何完成它的大纲是什么?我知道这个问题可能需要一些研究,所以当它可用于赏金时,我可以为其添加 500 点赏金以鼓励回答。 (请注意,所使用的底层语言是 C++-compiled-to-wasm,但我认为这对这个问题不重要。)

我想一种可能是这样的:https://rreverser.com/webassembly-shell-with-a-real-filesystem-access-in-a-browser/.

Javascript 文件 API

通过研究 File API 结果发现,当 读取文件时 浏览器将始终处理您 Blob。这给人的印象是所有该文件由浏览器获取到 RAM。 Blob 还有一个 .stream() 函数,returns 一个 ReadableStream 流式传输完全相同的 Blob.

事实证明(至少在 Chrome 中)处理的 Blob 是虚拟的,并且在请求之前不会加载基础文件。文件对象切片和实例化 reader 都不会加载整个文件:

file.slice(file.size - 100)
(await reader.read()).value.slice(0, 100)

Here is a test Sandbox and the sourcecode

该示例让您 select 文件广告将显示最后 100 个字符(使用 .slice())和前 100 个使用 ReadableStream(注意流函数没有 seek 功能)

我已经测试了 10GB(最大的 .csv 我有)并且浏览器没有消耗 RAM

这回答了问题的第一部分。由于能够在不消耗 RAM 的情况下流式传输(或执行分块访问)文件,您可以使用任意大的文件并搜索您的内容(二进制搜索或 table 扫描)。

Webassembly

在使用 stdwebRust 中没有 .read() 功能(因此无法流式传输内容)。但是 File 确实有 .slice() 函数来切片底层 blob(与 javascript 相同)。这是一个最小的工作示例:

#[macro_use]
extern crate stdweb;

use stdweb::js_export;

use std::convert::From;
use stdweb::web::IBlob;
use stdweb::web::File;
use stdweb::web::FileReader;
use stdweb::web::FileReaderResult;

#[js_export]
fn read_file(file: File) {
    let blob = file.slice(..2048);
    let len = stdweb::Number::from(blob.len() as f64);

    js! {
        var _len = @{len};
        console.log("length=" + _len);
        var _blob = @{blob};
        console.log(_blob);
    }
}

fn main() {
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WASM</title>
</head>
<body>
    <input type="file" id="field" />

    <script src="the_compiled_wasm_binding.js"></script>
    <script>
        async function onChange(e) {
            const files = e.target.files;
            if (files.length === 0) return;
            const file = files[0];

            // Slice
            Rust.the_compiled_wasm_binding.then(module => {
                module.read_file(file);
            })
        }

        document.getElementById("field").onchange = onChange;
    </script>
</body>
</html>

.slice() 函数的行为与 javascript 中的相同(整个文件未加载到 RAM 中)因此您可以在 WASM 中加载文件块并执行搜索。

请注意,slice()stdweb 实现使用 slice_blob(),它在内部执行:

js! (
    return @{reference}.slice(@{start}, @{end}, @{content_type});
).try_into().unwrap()

如您所见,它在底层使用了 javascript,所以这里没有优化。

结论

恕我直言,文件读取实施在 javascript 中更有效,原因是:

  • stdweb::File API 在引擎盖下使用原始 javascript(因此不会更快)
  • stdweb::Filejavascript 对应的功能少(缺少流式传输和其他功能很少)。

那么搜索算法 could/should 确实在 WASM 中实现了。该算法可以直接处理一个chunk(Blob)待处理。