diff --git a/package-lock.json b/package-lock.json index b6465a5..0873b8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "LGPL-3.0-or-later", "devDependencies": { "@eslint/js": "^10.0.1", - "@types/emscripten": "^1.40.0", + "@types/emscripten": "^1.41.6", "eslint": "^10.1.0", "globals": "^17.0.0", "prettier": "^3.2.5", @@ -289,9 +289,9 @@ "license": "MIT" }, "node_modules/@types/emscripten": { - "version": "1.41.5", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", - "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.6.tgz", + "integrity": "sha512-uN+9i8bFT5CUcZfyIEYDrSueACEyKGbUs5kC/72DGlZZoinh84sJfVV0i8UOJD1asdzkvLPBRrKs41kZ8MdEXg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 1ad2584..bc3f248 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/emscripten": "^1.40.0", + "@types/emscripten": "^1.41.6", "eslint": "^10.1.0", "globals": "^17.0.0", "prettier": "^3.2.5", diff --git a/src/plugin.ts b/src/plugin.ts index d0dfe6a..354380e 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -59,9 +59,10 @@ export default class EmscriptenPlugin implements EmscriptenNodeFS { if (!this.em_fs.isDir(mode) && !this.em_fs.isFile(mode) && !this.em_fs.isLink(mode)) { throw new this.em_fs.ErrnoError(toEmscriptenErrno(Errno.EINVAL)); } - const node: Node = new this.em_fs.FSNode(parent!, name, mode, rdev!); + const node: Node = new this.em_fs.FSNode(parent, name, mode, rdev); node.node_ops = this.node_ops; node.stream_ops = this.stream_ops; + this.em_fs.hashAddNode(node); return node; } diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index 371d509..a16dae2 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -34,12 +34,32 @@ class FSNode { } } +const hashed: EmFS.FSNode[] = []; + +// FS.nameTable and friends, straight out of emscripten's src/lib/libfs.js, since +// what the cache is worth can't be seen without the thing that reads it. +const nameTable: (EmFS.FSNode | undefined)[] = new Array(4096); + +function hashName(parentid: number, name: string): number { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + } + return ((parentid + hash) >>> 0) % nameTable.length; +} + const em_fs = { ErrnoError, FSNode, isDir: (mode: number) => (mode & 0o170000) === 0o040000, isFile: (mode: number) => (mode & 0o170000) === 0o100000, isLink: (mode: number) => (mode & 0o170000) === 0o120000, + hashAddNode: (node: EmFS.FSNode) => { + hashed.push(node); + const hash = hashName(node.parent.id, node.name); + node.name_next = nameTable[hash]; + nameTable[hash] = node; + }, } as unknown as typeof EmFS; mount('/zen', InMemory.create({ label: 'plugin-test' })); @@ -102,3 +122,48 @@ suite('errnos crossing into Emscripten', () => { assert.equal(plugin.node_ops.getattr(node).size, 5); }); }); + +/** FS.lookupNode: the table first, node_ops.lookup only when it misses. */ +function lookupNode(parent: EmFS.FSNode, name: string): EmFS.FSNode { + for (let node = nameTable[hashName(parent.id, name)]; node; node = node.name_next) { + if (node.parent.id === parent.id && node.name === name) return node; + } + return plugin.node_ops.lookup(parent, name); +} + +/** And FS.lookupPath, for a path with no symlinks or mounts in it. */ +function resolve(root: EmFS.FSNode, path: string): EmFS.FSNode { + let current = root; + for (const part of path.split('/')) current = lookupNode(current, part); + return current; +} + +suite("Emscripten's lookup cache", () => { + test('a created node is hashed into it #7', () => { + const node = plugin.createNode(null, 'hashed.txt', 0o100644); + assert.ok(hashed.includes(node)); + }); + + test('a resolved path is resolved once, not once per resolution #7', () => { + fs.mkdirSync('/zen/root/a/b/c/d', { recursive: true }); + fs.writeFileSync('/zen/root/a/b/c/d/leaf.txt', 'x'); + + const root = rooted(); + (root as unknown as FSNode).mount = { opts: { root: '/zen/root' } }; + + let lookups = 0; + const real = plugin.node_ops.lookup.bind(plugin.node_ops); + plugin.node_ops.lookup = (parent: EmFS.FSNode, name: string) => { + lookups++; + return real(parent, name); + }; + + try { + for (let i = 0; i < 100; i++) resolve(root, 'a/b/c/d/leaf.txt'); + } finally { + plugin.node_ops.lookup = real; + } + + assert.equal(lookups, 5, '5 components, resolved once each — not 500'); + }); +});