@zenfs/core 2.7.3-newstreams.0 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.7.3-newstreams.0",
3
+ "version": "2.7.4",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -69,13 +69,14 @@
69
69
  "lint": "eslint src tests",
70
70
  "test": "npx -s zenfs-test --clean; npx -s zenfs-test -abcpn; tests/fetch/run.sh; npx -s zenfs-test --report-only",
71
71
  "test:native": "npx -s zenfs-test tests/native-setup.ts --verbose",
72
+ "test:perf": "zbench",
72
73
  "build": "tsc --build",
73
74
  "dev": "tsc --build --watch",
74
75
  "prepublishOnly": "npm run build"
75
76
  },
76
77
  "dependencies": {
77
78
  "@types/node": "^26.0.0",
78
- "@zenfs/streams": "^0.0.1",
79
+ "@zenfs/streams": "^1.0.0",
79
80
  "buffer": "^6.0.3",
80
81
  "eventemitter3": "^5.0.1",
81
82
  "kerium": "^1.5.0",
@@ -91,7 +92,8 @@
91
92
  "prettier": "^3.2.5",
92
93
  "typedoc": "^0.28.18",
93
94
  "typescript": "^6.0.0",
94
- "typescript-eslint": "^8.58.0"
95
+ "typescript-eslint": "^8.58.0",
96
+ "zbench-js": "^0.1.0"
95
97
  },
96
98
  "prettier": {
97
99
  "singleQuote": true,
@@ -0,0 +1,34 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // Sequential appends into one region. Every write extends the same region, so a region that
3
+ // allocates exactly what it needs copies the whole file on each append.
4
+ import type { FileHandle } from '@zenfs/core/promises';
5
+ import { fs, mount, piece } from './common.ts';
6
+
7
+ interface Config {
8
+ writes: number;
9
+ }
10
+
11
+ interface State {
12
+ data: Buffer;
13
+ handle?: FileHandle;
14
+ }
15
+
16
+ export async function setup(): Promise<State> {
17
+ await mount('perf-append');
18
+ return { data: Buffer.alloc(piece, 0x11) };
19
+ }
20
+
21
+ export async function before(config: Config, state: State): Promise<void> {
22
+ state.handle = await fs.promises.open('/a', 'w');
23
+ }
24
+
25
+ export async function test(config: Config, state: State) {
26
+ for (let i = 0; i < config.writes; i++) await state.handle!.write(state.data, 0, state.data.length, i * piece);
27
+
28
+ return { writes: config.writes, bytes: config.writes * piece };
29
+ }
30
+
31
+ export async function after(config: Config, state: State): Promise<void> {
32
+ await state.handle!.close();
33
+ await fs.promises.rm('/a');
34
+ }
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { configureSingle, fs, InMemory } from '@zenfs/core';
3
+
4
+ export { fs };
5
+
6
+ export const KB = 1024;
7
+ export const MB = 1024 * 1024;
8
+
9
+ /**
10
+ * A piece and the stride between pieces, chosen so consecutive writes never merge into one region:
11
+ * the 12 KB hole between them is wider than `regionGapThreshold`.
12
+ */
13
+ export const piece = 4 * KB;
14
+ export const stride = 16 * KB;
15
+
16
+ /** A fresh in-memory filesystem, so no configuration inherits another's cached vnodes. */
17
+ export async function mount(name: string): Promise<void> {
18
+ await configureSingle({ backend: InMemory, name });
19
+ }
20
+
21
+ /** Split a buffer the way a stream writer feeds it. */
22
+ export function chunks(buffer: Buffer, size: number): Buffer[] {
23
+ const out: Buffer[] = [];
24
+ for (let i = 0; i < buffer.length; i += size) out.push(buffer.subarray(i, Math.min(i + size, buffer.length)));
25
+ return out;
26
+ }
27
+
28
+ /** Give the event loop a moment, so work queued by the previous phase never lands in the timed one. */
29
+ export function settle(): Promise<void> {
30
+ return new Promise(resolve => setTimeout(resolve, 50));
31
+ }
@@ -0,0 +1,84 @@
1
+ {
2
+ "flags": {
3
+ "prep": ["writeFile", "stream"]
4
+ },
5
+ "tests": [
6
+ {
7
+ "path": "stream-read.ts",
8
+ "name": "Read with createReadStream",
9
+ "flags": ["prep"],
10
+ "measure": {
11
+ "bytes": { "throughput": "s", "aggregate_throughput": "s", "unit": "MB" },
12
+ "files": { "cost": true, "unit": "file" }
13
+ },
14
+ "configurations": [
15
+ { "name": "4K x2000", "value": { "size": 4096, "count": 2000, "hwm": 65536 } },
16
+ { "name": "256K x200", "value": { "size": 262144, "count": 200, "hwm": 65536 } },
17
+ { "name": "8M x8", "cpu": 1, "mem": 1, "value": { "size": 8388608, "count": 8, "hwm": 65536 } },
18
+ {
19
+ "name": "8M x8, hwm 1M",
20
+ "cpu": 1,
21
+ "mem": 1,
22
+ "value": { "size": 8388608, "count": 8, "hwm": 1048576 }
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "path": "stream-write.ts",
28
+ "name": "Write with createWriteStream",
29
+ "measure": {
30
+ "bytes": { "throughput": "s", "aggregate_throughput": "s", "unit": "MB" },
31
+ "files": { "cost": true, "unit": "file" }
32
+ },
33
+ "configurations": [
34
+ { "name": "4K x2000", "value": { "size": 4096, "count": 2000, "hwm": 65536 } },
35
+ { "name": "256K x200", "value": { "size": 262144, "count": 200, "hwm": 65536 } },
36
+ { "name": "8M x8", "cpu": 1, "mem": 1, "value": { "size": 8388608, "count": 8, "hwm": 65536 } },
37
+ {
38
+ "name": "8M x8, hwm 1M",
39
+ "cpu": 1,
40
+ "mem": 1,
41
+ "value": { "size": 8388608, "count": 8, "hwm": 1048576 }
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "path": "fragmented-read.ts",
47
+ "name": "Read across many cached regions",
48
+ "measure": {
49
+ "reads": { "cost": true, "aggregate_cost": true, "unit": "read" }
50
+ },
51
+ "configurations": [
52
+ { "name": "128 regions", "value": { "regions": 128, "reads": 4000 } },
53
+ { "name": "512 regions", "value": { "regions": 512, "reads": 4000 } },
54
+ { "name": "2048 regions", "cpu": 1, "value": { "regions": 2048, "reads": 4000 } },
55
+ { "name": "8192 regions", "cpu": 2, "mem": 1, "value": { "regions": 8192, "reads": 4000 } }
56
+ ]
57
+ },
58
+ {
59
+ "path": "fragmented-write.ts",
60
+ "name": "Scattered writes into one file",
61
+ "measure": {
62
+ "writes": { "cost": true, "aggregate_cost": true, "unit": "write" }
63
+ },
64
+ "configurations": [
65
+ { "name": "512 writes", "value": { "writes": 512 } },
66
+ { "name": "2048 writes", "cpu": 1, "value": { "writes": 2048 } },
67
+ { "name": "8192 writes", "cpu": 2, "mem": 1, "value": { "writes": 8192 } }
68
+ ]
69
+ },
70
+ {
71
+ "path": "append.ts",
72
+ "name": "Append 4 KB at a time",
73
+ "measure": {
74
+ "bytes": { "throughput": "s", "unit": "MB" },
75
+ "writes": { "cost": true, "aggregate_cost": true, "unit": "write" }
76
+ },
77
+ "configurations": [
78
+ { "name": "500 appends", "value": { "writes": 500 } },
79
+ { "name": "2000 appends", "value": { "writes": 2000 } },
80
+ { "name": "8000 appends", "cpu": 1, "value": { "writes": 8000 } }
81
+ ]
82
+ }
83
+ ]
84
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // Reads spread across a file with many disjoint cached regions.
3
+
4
+ import type { FileHandle } from '@zenfs/core/promises';
5
+ import { fs, mount, piece, stride } from './common.ts';
6
+
7
+ interface Config {
8
+ /** Cached regions to build, one per scattered write */
9
+ regions: number;
10
+ reads: number;
11
+ }
12
+
13
+ interface State {
14
+ handle: FileHandle;
15
+ target: Buffer;
16
+ }
17
+
18
+ export async function setup(config: Config): Promise<State> {
19
+ await mount('perf-fragmented-read');
20
+
21
+ await fs.promises.writeFile('/f', Buffer.alloc(config.regions * stride));
22
+ const handle = await fs.promises.open('/f', 'r+');
23
+
24
+ const data = Buffer.alloc(piece, 0xcd);
25
+ for (let i = 0; i < config.regions; i++) await handle.write(data, 0, data.length, i * stride);
26
+
27
+ return { handle, target: Buffer.allocUnsafe(piece) };
28
+ }
29
+
30
+ export async function test(config: Config, state: State) {
31
+ // Backwards, so a scan that starts at region zero pays its worst case on every read
32
+ for (let i = 0; i < config.reads; i++)
33
+ await state.handle.read(state.target, 0, state.target.length, (config.regions - 1 - (i % config.regions)) * stride);
34
+
35
+ return { reads: config.reads, bytes: config.reads * piece };
36
+ }
37
+
38
+ export async function teardown(config: Config, state: State): Promise<void> {
39
+ await state.handle.close();
40
+ await fs.promises.rm('/f');
41
+ }
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // Writes that each land in their own region, so the region list grows by one per write.
3
+ // Anything that walks the whole list — or re-sorts the dirty ranges — per write shows up as
4
+ // quadratic here.
5
+ import type { FileHandle } from '@zenfs/core/promises';
6
+ import { fs, mount, piece, stride } from './common.ts';
7
+
8
+ interface Config {
9
+ writes: number;
10
+ }
11
+
12
+ interface State {
13
+ data: Buffer;
14
+ handle?: FileHandle;
15
+ }
16
+
17
+ export async function setup(): Promise<State> {
18
+ await mount('perf-fragmented-write');
19
+ return { data: Buffer.alloc(piece, 0xcd) };
20
+ }
21
+
22
+ /** A fresh file each iteration: the point is the cost of building the region list, not of reusing it. */
23
+ export async function before(config: Config, state: State): Promise<void> {
24
+ await fs.promises.writeFile('/f', Buffer.alloc(config.writes * stride));
25
+ state.handle = await fs.promises.open('/f', 'r+');
26
+ }
27
+
28
+ export async function test(config: Config, state: State) {
29
+ for (let i = 0; i < config.writes; i++) await state.handle!.write(state.data, 0, state.data.length, i * stride);
30
+
31
+ return { writes: config.writes, bytes: config.writes * piece };
32
+ }
33
+
34
+ export async function after(config: Config, state: State): Promise<void> {
35
+ await state.handle!.close();
36
+ await fs.promises.rm('/f');
37
+ }
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // Reading through `createReadStream`: the VFS cache lookup, `copyFromCache`, and the stream's own
3
+ // chunking, with the files written outside the timed window.
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { chunks, fs, mount, settle } from './common.ts';
6
+
7
+ interface Config {
8
+ size: number;
9
+ count: number;
10
+ /** Stream highWaterMark, which is also the read chunk size */
11
+ hwm: number;
12
+ prep: 'writeFile' | 'stream';
13
+ }
14
+
15
+ interface State {
16
+ buffer: Buffer;
17
+ parts: Buffer[];
18
+ }
19
+
20
+ export async function setup(config: Config): Promise<State> {
21
+ await mount('perf-stream-read');
22
+ const buffer = Buffer.alloc(config.size, 0xab);
23
+ return { buffer, parts: chunks(buffer, config.hwm) };
24
+ }
25
+
26
+ export async function before(config: Config, state: State): Promise<void> {
27
+ for (let i = 0; i < config.count; i++) {
28
+ if (config.prep == 'stream') await pipeline(state.parts, fs.createWriteStream(`/w${i}`, { highWaterMark: config.hwm }));
29
+ else await fs.promises.writeFile(`/w${i}`, state.buffer);
30
+ }
31
+ await settle();
32
+ }
33
+
34
+ export async function test(config: Config) {
35
+ let bytes = 0;
36
+
37
+ for (let i = 0; i < config.count; i++) {
38
+ for await (const chunk of fs.createReadStream(`/w${i}`, { highWaterMark: config.hwm })) bytes += (chunk as Buffer).length;
39
+ }
40
+
41
+ if (bytes != config.size * config.count) throw new Error(`short read: ${bytes} of ${config.size * config.count}`);
42
+
43
+ return { bytes, files: config.count };
44
+ }
45
+
46
+ export async function after(config: Config): Promise<void> {
47
+ for (let i = 0; i < config.count; i++) await fs.promises.rm(`/w${i}`);
48
+ }
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // Writing through `createWriteStream`: how the cache grows as a file is appended to one chunk at a
3
+ // time. A region that reallocates exactly on every append makes this quadratic in the file's size.
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { chunks, fs, mount, settle } from './common.ts';
6
+
7
+ interface Config {
8
+ size: number;
9
+ count: number;
10
+ hwm: number;
11
+ }
12
+
13
+ interface State {
14
+ parts: Buffer[];
15
+ }
16
+
17
+ export async function setup(config: Config): Promise<State> {
18
+ await mount('perf-stream-write');
19
+ return { parts: chunks(Buffer.alloc(config.size, 0xab), config.hwm) };
20
+ }
21
+
22
+ export async function test(config: Config, state: State) {
23
+ for (let i = 0; i < config.count; i++) await pipeline(state.parts, fs.createWriteStream(`/w${i}`, { highWaterMark: config.hwm }));
24
+
25
+ return { bytes: config.size * config.count, files: config.count };
26
+ }
27
+
28
+ export async function after(config: Config): Promise<void> {
29
+ for (let i = 0; i < config.count; i++) await fs.promises.rm(`/w${i}`);
30
+ await settle();
31
+ }