Skip to content

Commit e527a85

Browse files
committed
chunked backend, works for single chunks and simple backend url mapping
1 parent 27d7daa commit e527a85

File tree

3 files changed

+137
-47
lines changed

3 files changed

+137
-47
lines changed

src/library_wasmfs_fetch.js

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,24 @@
55
*/
66

77
addToLibrary({
8+
$wasmFS$JSMemoryRanges: {},
9+
810
// Fetch backend: On first access of the file (either a read or a getSize), it
911
// will fetch() the data from the network asynchronously. Otherwise, after
1012
// that fetch it behaves just like JSFile (and it reuses the code from there).
1113

1214
_wasmfs_create_fetch_backend_js__deps: [
1315
'$wasmFS$backends',
14-
'$wasmFS$JSMemoryFiles',
15-
'_wasmfs_create_js_file_backend_js',
16-
'_wasmfs_fetch_get_file_path',
16+
'$wasmFS$JSMemoryRanges',
17+
'_wasmfs_fetch_get_file_url',
18+
'_wasmfs_fetch_get_chunk_size',
1719
],
1820
_wasmfs_create_fetch_backend_js: async function(backend) {
1921
// Get a promise that fetches the data and stores it in JS memory (if it has
2022
// not already been fetched).
21-
async function getFile(file) {
22-
if (wasmFS$JSMemoryFiles[file]) {
23-
// The data is already here, so nothing to do before we continue on to
24-
// the actual read below.
25-
return Promise.resolve();
26-
}
27-
// This is the first time we want the file's data.
23+
async function getFileRange(file, offset, len) {
2824
var url = '';
29-
var fileUrl_p = __wasmfs_fetch_get_file_path(file);
25+
var fileUrl_p = __wasmfs_fetch_get_file_url(file);
3026
var fileUrl = UTF8ToString(fileUrl_p);
3127
var isAbs = fileUrl.indexOf('://') !== -1;
3228
if (isAbs) {
@@ -38,31 +34,68 @@ addToLibrary({
3834
} catch (e) {
3935
}
4036
}
41-
var response = await fetch(url);
37+
var chunkSize = __wasmfs_fetch_get_chunk_size();
38+
offset = offset || 0;
39+
len = len || chunkSize;
40+
var firstChunk = (offset / chunkSize) | 0;
41+
var lastChunk = ((offset+len) / chunkSize) | 0;
42+
if (!(file in wasmFS$JSMemoryRanges)) {
43+
var fileInfo = await fetch(url,{method:"HEAD",headers:{"Range":"bytes=0-1"}});
44+
if(fileInfo.ok && fileInfo.headers.has("Content-Length") && fileInfo.headers.get("Accept-Ranges") == "bytes" && (parseInt(fileInfo.headers.get("Content-Length")) > chunkSize*2)) {
45+
wasmFS$JSMemoryRanges[file] = {size:parseInt(fileInfo.headers.get("Content-Length")), chunks:[], chunkSize:chunkSize};
46+
} else {
47+
// may as well/forced to download the whole file
48+
var wholeFileReq = await fetch(url);
49+
if(!wholeFileReq.ok) {
50+
throw wholeFileReq;
51+
}
52+
var wholeFileData = new Uint8Array(await wholeFileReq.arrayBuffer());
53+
var text = new TextDecoder().decode(wholeFileData);
54+
wasmFS$JSMemoryRanges[file] = {size:wholeFileData.byteLength, chunks:[wholeFileData], chunkSize:wholeFileData.byteLength};
55+
return Promise.resolve();
56+
}
57+
}
58+
var allPresent = true;
59+
var i;
60+
if(lastChunk * chunkSize < offset+len) {
61+
lastChunk += 1;
62+
}
63+
for(i = firstChunk; i < lastChunk; i++) {
64+
if(!wasmFS$JSMemoryRanges[file].chunks[i]) {
65+
allPresent = false;
66+
break;
67+
}
68+
}
69+
if (allPresent) {
70+
// The data is already here, so nothing to do before we continue on to
71+
// the actual read.
72+
return Promise.resolve();
73+
}
74+
// This is the first time we want the chunk's data.
75+
var start = firstChunk*chunkSize;
76+
var end = lastChunk*chunkSize;
77+
var response = await fetch(url, {headers:{"Range": `bytes=${start}-${end}`}});
4278
if (response.ok) {
43-
var buffer = await response['arrayBuffer']();
44-
wasmFS$JSMemoryFiles[file] = new Uint8Array(buffer);
79+
var bytes = new Uint8Array(await response['arrayBuffer']());
80+
for (i = firstChunk; i < lastChunk; i++) {
81+
wasmFS$JSMemoryRanges[file].chunks[i] = bytes.slice(i*chunkSize,(i+1)*chunkSize);
82+
}
4583
} else {
4684
throw response;
4785
}
86+
return Promise.resolve();
4887
}
4988

50-
// Start with the normal JSFile operations. This sets
51-
// wasmFS$backends[backend]
52-
// which we will then augment.
53-
__wasmfs_create_js_file_backend_js(backend);
54-
55-
// Add the async operations on top.
56-
var jsFileOps = wasmFS$backends[backend];
5789
wasmFS$backends[backend] = {
5890
// alloc/free operations are not actually async. Just forward to the
5991
// parent class, but we must return a Promise as the caller expects.
6092
allocFile: async (file) => {
61-
jsFileOps.allocFile(file);
93+
// nop
6294
return Promise.resolve();
6395
},
6496
freeFile: async (file) => {
65-
jsFileOps.freeFile(file);
97+
// free memory
98+
wasmFS$JSMemoryRanges[file] = undefined;
6699
return Promise.resolve();
67100
},
68101

@@ -73,17 +106,39 @@ addToLibrary({
73106
// read/getSize fetch the data, then forward to the parent class.
74107
read: async (file, buffer, length, offset) => {
75108
try {
76-
await getFile(file);
109+
await getFileRange(file, offset || 0, length);
77110
} catch (response) {
78111
return response.status === 404 ? -{{{ cDefs.ENOENT }}} : -{{{ cDefs.EBADF }}};
79112
}
80-
return jsFileOps.read(file, buffer, length, offset);
113+
var fileInfo = wasmFS$JSMemoryRanges[file];
114+
var fileData = fileInfo.chunks;
115+
var chunkSize = fileInfo.chunkSize;
116+
var firstChunk = (offset / chunkSize) | 0;
117+
var lastChunk = ((offset+length) / chunkSize) | 0;
118+
if(offset + length > lastChunk * chunkSize) {
119+
lastChunk += 1;
120+
}
121+
var readLength = 0;
122+
for (var i = firstChunk; i < lastChunk; i++) {
123+
var chunk = fileData[i];
124+
var start = Math.max(i*chunkSize, offset);
125+
if(!chunk) {
126+
throw [fileData.length, firstChunk, lastChunk, i];
127+
}
128+
var end = Math.min(start+chunk.byteLength, offset+length);
129+
var chunkStart = i*chunkSize;
130+
HEAPU8.set(chunk.subarray(start-chunkStart, end-chunkStart), buffer+(start-offset));
131+
readLength = end - offset;
132+
}
133+
return readLength;
81134
},
82135
getSize: async (file) => {
83136
try {
84-
await getFile(file);
85-
} catch (response) {}
86-
return jsFileOps.getSize(file);
137+
await getFileRange(file, 0, 0);
138+
} catch (response) {
139+
return 0;
140+
}
141+
return wasmFS$JSMemoryRanges[file].size;
87142
},
88143
};
89144
},

system/lib/wasmfs/backends/fetch_backend.cpp

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,41 @@
1313

1414
namespace wasmfs {
1515

16+
const uint32_t DEFAULT_CHUNK_SIZE = 16*1024*1024;
17+
18+
class FetchBackend : public wasmfs::ProxiedAsyncJSBackend {
19+
std::string baseUrl;
20+
uint32_t chunkSize;
21+
public:
22+
FetchBackend(const std::string& baseUrl,
23+
uint32_t chunkSize,
24+
std::function<void(backend_t)> setupOnThread)
25+
: ProxiedAsyncJSBackend(setupOnThread), baseUrl(baseUrl), chunkSize(chunkSize)
26+
// TODO manifest
27+
{}
28+
std::shared_ptr<DataFile> createFile(mode_t mode) override;
29+
std::shared_ptr<Directory> createDirectory(mode_t mode) override;
30+
const std::string getFileURL(const std::string& filePath);
31+
uint32_t getChunkSize();
32+
};
33+
34+
1635
class FetchFile : public ProxiedAsyncJSImplFile {
1736
std::string filePath;
37+
std::string fileUrl;
1838

1939
public:
2040
FetchFile(const std::string& path,
2141
mode_t mode,
2242
backend_t backend,
2343
emscripten::ProxyWorker& proxy)
24-
: ProxiedAsyncJSImplFile(mode, backend, proxy), filePath(path) {}
44+
: ProxiedAsyncJSImplFile(mode, backend, proxy), filePath(path) {
45+
this->fileUrl = dynamic_cast<FetchBackend*>(getBackend())->getFileURL(filePath);
46+
}
2547

2648
const std::string& getPath() const { return filePath; }
49+
const std::string& getURL() const { return fileUrl; }
50+
const uint32_t getChunkSize() const { return dynamic_cast<FetchBackend*>(getBackend())->getChunkSize(); }
2751
};
2852

2953
class FetchDirectory : public MemoryDirectory {
@@ -60,38 +84,50 @@ class FetchDirectory : public MemoryDirectory {
6084
}
6185
};
6286

63-
class FetchBackend : public ProxiedAsyncJSBackend {
64-
std::string baseUrl;
65-
66-
public:
67-
FetchBackend(const std::string& baseUrl,
68-
std::function<void(backend_t)> setupOnThread)
69-
: ProxiedAsyncJSBackend(setupOnThread), baseUrl(baseUrl) {}
87+
std::shared_ptr<DataFile> FetchBackend::createFile(mode_t mode) {
88+
return std::make_shared<FetchFile>("", mode, this, proxy);
89+
}
7090

71-
std::shared_ptr<DataFile> createFile(mode_t mode) override {
72-
return std::make_shared<FetchFile>(baseUrl, mode, this, proxy);
73-
}
91+
std::shared_ptr<Directory> FetchBackend::createDirectory(mode_t mode) {
92+
return std::make_shared<FetchDirectory>("", mode, this, proxy);
93+
}
7494

75-
std::shared_ptr<Directory> createDirectory(mode_t mode) override {
76-
return std::make_shared<FetchDirectory>(baseUrl, mode, this, proxy);
95+
const std::string FetchBackend::getFileURL(const std::string& filePath) {
96+
// TODO use manifest
97+
if(filePath == "") {
98+
return baseUrl;
7799
}
78-
};
100+
return baseUrl + "/" + filePath;
101+
}
102+
uint32_t FetchBackend::getChunkSize() {
103+
return chunkSize;
104+
}
79105

80106
extern "C" {
81-
backend_t wasmfs_create_fetch_backend(const char* base_url) {
107+
backend_t wasmfs_create_fetch_backend(const char* base_url /* TODO manifest */) {
82108
// ProxyWorker cannot safely be synchronously spawned from the main browser
83109
// thread. See comment in thread_utils.h for more details.
84110
assert(!emscripten_is_main_browser_thread() &&
85111
"Cannot safely create fetch backend on main browser thread");
86112
return wasmFS.addBackend(std::make_unique<FetchBackend>(
87113
base_url ? base_url : "",
114+
DEFAULT_CHUNK_SIZE,
115+
/* TODO manifest */
88116
[](backend_t backend) { _wasmfs_create_fetch_backend_js(backend); }));
89-
}
117+
}
90118

91119
const char* EMSCRIPTEN_KEEPALIVE _wasmfs_fetch_get_file_path(void* ptr) {
92120
auto* file = reinterpret_cast<wasmfs::FetchFile*>(ptr);
93121
return file ? file->getPath().data() : nullptr;
94122
}
123+
const char* EMSCRIPTEN_KEEPALIVE _wasmfs_fetch_get_file_url(void* ptr) {
124+
auto* file = reinterpret_cast<wasmfs::FetchFile*>(ptr);
125+
return file ? file->getURL().data() : nullptr;
126+
}
127+
uint32_t EMSCRIPTEN_KEEPALIVE _wasmfs_fetch_get_chunk_size(void* ptr) {
128+
auto* file = reinterpret_cast<wasmfs::FetchFile*>(ptr);
129+
return file ? file->getChunkSize() : DEFAULT_CHUNK_SIZE;
130+
}
95131
}
96132

97133
} // namespace wasmfs

system/lib/wasmfs/backends/fetch_backend.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
#include "wasmfs.h"
77

88
extern "C" {
9-
10-
// See library_wasmfs_fetch.js
11-
void _wasmfs_create_fetch_backend_js(wasmfs::backend_t);
9+
// See library_wasmfs_fetch.js
10+
void _wasmfs_create_fetch_backend_js(wasmfs::backend_t);
1211
}

0 commit comments

Comments
 (0)