Skip to content

Commit 91b348d

Browse files
authored
Add mount option { autoPersist: true } to IDBFS mount. (#21938)
* Add new setting -sIDBFS_AUTO_PERSIST which changes the semantics of an IndedexDB mount to automatically persist the VFS to IndexedDB after closing any file that has been written to. This enables users to avoid needing to call fsync() to persist files to the filesystem. * Kick off persisting the filesystem on more operations. Fix test_idbfs_sync to properly delete IndexedDB state for the test if test fails. * Update settings_reference.rst * Propagate injected node_ops to the newly created child node * Add [link] directive * Move IDBFS_AUTO_PERSIST to a mount option rather than a -sSETTING. * Address review. * Add ChangeLog.md entry.
1 parent 6491f02 commit 91b348d

File tree

5 files changed

+111
-16
lines changed

5 files changed

+111
-16
lines changed

ChangeLog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ See docs/process.md for more on how version tagging works.
2323
- The JSPI feature now uses the updated browser API for JSPI (available in
2424
Chrome v126+). To support older versions of Chrome use Emscripten version
2525
3.1.60 or earlier.
26+
- IDBFS mount has gained a new option { autoPersist: true }, which if passed,
27+
changes the semantics of the IDBFS mount to automatically persist any changes
28+
made to the filesystem. (#21938)
2629

2730
3.1.60 - 05/20/24
2831
-----------------

site/source/docs/api_reference/Filesystem-API.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ The *IDBFS* file system implements the :js:func:`FS.syncfs` interface, which whe
113113

114114
This is provided to overcome the limitation that browsers do not offer synchronous APIs for persistent storage, and so (by default) all writes exist only temporarily in-memory.
115115

116+
If the mount option `autoPersist: true` is passed when mounting IDBFS, then whenever any changes are made to the IDBFS directory tree, they will be automatically persisted to the IndexedDB backend. This lets users avoid needing to manually call `FS.syncfs` to persist changes to the IDBFS mounted directory tree.
117+
116118
.. _filesystem-api-workerfs:
117119

118120
WORKERFS

src/library_idbfs.js

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,83 @@ addToLibrary({
2323
},
2424
DB_VERSION: 21,
2525
DB_STORE_NAME: 'FILE_DATA',
26-
// reuse all of the core MEMFS functionality
27-
mount: (...args) => MEMFS.mount(...args),
26+
27+
// Queues a new VFS -> IDBFS synchronization operation
28+
queuePersist: (mount) => {
29+
function onPersistComplete() {
30+
if (mount.idbPersistState === 'again') startPersist(); // If a new sync request has appeared in between, kick off a new sync
31+
else mount.idbPersistState = 0; // Otherwise reset sync state back to idle to wait for a new sync later
32+
}
33+
function startPersist() {
34+
mount.idbPersistState = 'idb'; // Mark that we are currently running a sync operation
35+
IDBFS.syncfs(mount, /*populate:*/false, onPersistComplete);
36+
}
37+
38+
if (!mount.idbPersistState) {
39+
// Programs typically write/copy/move multiple files in the in-memory
40+
// filesystem within a single app frame, so when a filesystem sync
41+
// command is triggered, do not start it immediately, but only after
42+
// the current frame is finished. This way all the modified files
43+
// inside the main loop tick will be batched up to the same sync.
44+
mount.idbPersistState = setTimeout(startPersist, 0);
45+
} else if (mount.idbPersistState === 'idb') {
46+
// There is an active IndexedDB sync operation in-flight, but we now
47+
// have accumulated more files to sync. We should therefore queue up
48+
// a new sync after the current one finishes so that all writes
49+
// will be properly persisted.
50+
mount.idbPersistState = 'again';
51+
}
52+
},
53+
54+
mount: (mount) => {
55+
// reuse core MEMFS functionality
56+
var mnt = MEMFS.mount(mount);
57+
// If the automatic IDBFS persistence option has been selected, then automatically persist
58+
// all modifications to the filesystem as they occur.
59+
if (mount?.opts?.autoPersist) {
60+
mnt.idbPersistState = 0; // IndexedDB sync starts in idle state
61+
var memfs_node_ops = mnt.node_ops;
62+
mnt.node_ops = Object.assign({}, mnt.node_ops); // Clone node_ops to inject write tracking
63+
mnt.node_ops.mknod = (parent, name, mode, dev) => {
64+
var node = memfs_node_ops.mknod(parent, name, mode, dev);
65+
// Propagate injected node_ops to the newly created child node
66+
node.node_ops = mnt.node_ops;
67+
// Remember for each IDBFS node which IDBFS mount point they came from so we know which mount to persist on modification.
68+
node.idbfs_mount = mnt.mount;
69+
// Remember original MEMFS stream_ops for this node
70+
node.memfs_stream_ops = node.stream_ops;
71+
// Clone stream_ops to inject write tracking
72+
node.stream_ops = Object.assign({}, node.stream_ops);
73+
74+
// Track all file writes
75+
node.stream_ops.write = (stream, buffer, offset, length, position, canOwn) => {
76+
// This file has been modified, we must persist IndexedDB when this file closes
77+
stream.node.isModified = true;
78+
return node.memfs_stream_ops.write(stream, buffer, offset, length, position, canOwn);
79+
};
80+
81+
// Persist IndexedDB on file close
82+
node.stream_ops.close = (stream) => {
83+
var n = stream.node;
84+
if (n.isModified) {
85+
IDBFS.queuePersist(n.idbfs_mount);
86+
n.isModified = false;
87+
}
88+
if (n.memfs_stream_ops.close) return n.memfs_stream_ops.close(stream);
89+
};
90+
91+
return node;
92+
};
93+
// Also kick off persisting the filesystem on other operations that modify the filesystem.
94+
mnt.node_ops.mkdir = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.mkdir(...args));
95+
mnt.node_ops.rmdir = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.rmdir(...args));
96+
mnt.node_ops.symlink = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.symlink(...args));
97+
mnt.node_ops.unlink = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.unlink(...args));
98+
mnt.node_ops.rename = (...args) => (IDBFS.queuePersist(mnt.mount), memfs_node_ops.rename(...args));
99+
}
100+
return mnt;
101+
},
102+
28103
syncfs: (mount, populate, callback) => {
29104
IDBFS.getLocalSet(mount, (err, local) => {
30105
if (err) return callback(err);

test/fs/test_idbfs_sync.c

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
int result = 1;
1717

18-
void success() {
18+
void report_result() {
1919
REPORT_RESULT(result);
2020
#ifdef FORCE_EXIT
2121
emscripten_force_exit(0);
@@ -133,7 +133,18 @@ void test() {
133133

134134
#endif
135135

136-
#if EXTRA_WORK
136+
// If the test failed, then delete test files from IndexedDB so that the test
137+
// runner will not leak test state to subsequent tests that reuse this same
138+
// file.
139+
if (result != 1) {
140+
unlink("/working1/empty.txt");
141+
unlink("/working1/waka.txt");
142+
unlink("/working1/moar.txt");
143+
rmdir("/working1/dir");
144+
EM_ASM(FS.syncfs(function(){})); // And persist deleted changes
145+
}
146+
147+
#if EXTRA_WORK && !FIRST
137148
EM_ASM(
138149
for (var i = 0; i < 100; i++) {
139150
FS.syncfs(function (err) {
@@ -144,24 +155,32 @@ void test() {
144155
);
145156
#endif
146157

158+
#ifdef IDBFS_AUTO_PERSIST
159+
report_result();
160+
#else
147161
// sync from memory state to persisted and then
148-
// run 'success'
162+
// run 'report_result'
149163
EM_ASM(
150164
// Ensure IndexedDB is closed at exit.
151165
Module['onExit'] = function() {
152166
assert(Object.keys(IDBFS.dbs).length == 0);
153167
};
154168
FS.syncfs(function (err) {
155169
assert(!err);
156-
ccall('success', 'v');
170+
ccall('report_result', 'v');
157171
});
158172
);
173+
#endif
159174
}
160175

161176
int main() {
162177
EM_ASM(
163178
FS.mkdir('/working1');
164-
FS.mount(IDBFS, {}, '/working1');
179+
FS.mount(IDBFS, {
180+
#ifdef IDBFS_AUTO_PERSIST
181+
autoPersist: true
182+
#endif
183+
}, '/working1');
165184

166185
#if !FIRST
167186
// syncfs(true, f) should not break on already-existing directories:

test/test_browser.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,18 +1354,14 @@ def test_fflush(self):
13541354
@parameterized({
13551355
'': ([],),
13561356
'extra': (['-DEXTRA_WORK'],),
1357+
'autopersist': (['-DIDBFS_AUTO_PERSIST'],),
1358+
'force_exit': (['-sEXIT_RUNTIME', '-DFORCE_EXIT'],),
13571359
})
1358-
def test_fs_idbfs_sync(self, extra):
1360+
def test_fs_idbfs_sync(self, args):
13591361
self.set_setting('DEFAULT_LIBRARY_FUNCS_TO_INCLUDE', '$ccall')
13601362
secret = str(time.time())
1361-
self.btest('fs/test_idbfs_sync.c', '1', args=['-lidbfs.js', '-DFIRST', f'-DSECRET="{secret}"', '-sEXPORTED_FUNCTIONS=_main,_test,_success', '-lidbfs.js'])
1362-
self.btest('fs/test_idbfs_sync.c', '1', args=['-lidbfs.js', f'-DSECRET="{secret }"', '-sEXPORTED_FUNCTIONS=_main,_test,_success', '-lidbfs.js'] + extra)
1363-
1364-
def test_fs_idbfs_sync_force_exit(self):
1365-
self.set_setting('DEFAULT_LIBRARY_FUNCS_TO_INCLUDE', '$ccall')
1366-
secret = str(time.time())
1367-
self.btest('fs/test_idbfs_sync.c', '1', args=['-lidbfs.js', '-DFIRST', f'-DSECRET="{secret}"', '-sEXPORTED_FUNCTIONS=_main,_test,_success', '-sEXIT_RUNTIME', '-DFORCE_EXIT', '-lidbfs.js'])
1368-
self.btest('fs/test_idbfs_sync.c', '1', args=['-lidbfs.js', f'-DSECRET="{secret }"', '-sEXPORTED_FUNCTIONS=_main,_test,_success', '-sEXIT_RUNTIME', '-DFORCE_EXIT', '-lidbfs.js'])
1363+
self.btest('fs/test_idbfs_sync.c', '1', args=['-lidbfs.js', f'-DSECRET="{secret}"', '-sEXPORTED_FUNCTIONS=_main,_test,_report_result', '-lidbfs.js'] + args + ['-DFIRST'])
1364+
self.btest('fs/test_idbfs_sync.c', '1', args=['-lidbfs.js', f'-DSECRET="{secret}"', '-sEXPORTED_FUNCTIONS=_main,_test,_report_result', '-lidbfs.js'] + args)
13691365

13701366
def test_fs_idbfs_fsync(self):
13711367
# sync from persisted state into memory before main()

0 commit comments

Comments
 (0)