Skip to content

Commit a3ef9f9

Browse files
authored
perf(fmt): use synchronous file I/O in workers (#142)
1 parent 6062b4a commit a3ef9f9

7 files changed

Lines changed: 27 additions & 105 deletions

File tree

packages/rstack/THIRD_PARTY_NOTICES.md

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,35 +88,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
8888
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
8989
SOFTWARE.
9090

91-
## atomically
92-
93-
This package includes bundled code from
94-
[atomically](https://github.com/fabiospampinato/atomically).
95-
96-
License: MIT
97-
98-
The MIT License (MIT)
99-
100-
Copyright (c) 2020-present Fabio Spampinato
101-
102-
Permission is hereby granted, free of charge, to any person obtaining a
103-
copy of this software and associated documentation files (the "Software"),
104-
to deal in the Software without restriction, including without limitation
105-
the rights to use, copy, modify, merge, publish, distribute, sublicense,
106-
and/or sell copies of the Software, and to permit persons to whom the
107-
Software is furnished to do so, subject to the following conditions:
108-
109-
The above copyright notice and this permission notice shall be included in
110-
all copies or substantial portions of the Software.
111-
112-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
113-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
114-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
115-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
116-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
117-
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
118-
DEALINGS IN THE SOFTWARE.
119-
12091
## fast-ignore
12192

12293
This package includes bundled code from [fast-ignore](https://github.com/fabiospampinato/fast-ignore).

packages/rstack/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"@rstest/adapter-rslib": "catalog:",
7070
"@types/micromatch": "catalog:",
7171
"@types/node": "catalog:",
72-
"atomically": "catalog:",
7372
"fast-ignore": "catalog:",
7473
"ignore": "catalog:",
7574
"import-meta-resolve": "catalog:",

packages/rstack/src/fmt/worker.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
// Derived from @prettier/cli, see THIRD_PARTY_NOTICES.md
22

3-
import { readFile, writeFile } from 'atomically';
3+
import { readFileSync, writeFileSync } from 'node:fs';
44
import { format } from 'prettier';
55
import { getPrettierPlugins } from './prettierPlugins.ts';
66
import type { FmtFileRequest } from './types.ts';
77

88
/**
9-
* Formatting output can be regenerated, so avoid waiting for a durability sync
10-
* after every file, which is especially expensive during parallel formatting.
11-
* `atomically` still uses a temporary file and rename for atomic replacement.
9+
* Use synchronous direct I/O inside the dedicated worker to avoid libuv
10+
* scheduling overhead. This prioritizes throughput over crash-safe replacement.
1211
*/
13-
const atomicWriteOptions = {
14-
encoding: 'utf8',
15-
fsync: false,
16-
} as const;
17-
1812
const formatFile = async (
1913
{ path, options }: FmtFileRequest,
2014
shouldWrite: boolean,
2115
): Promise<boolean> => {
22-
const source = await readFile(path, 'utf8');
16+
const source = readFileSync(path, 'utf8');
2317
const formatted = await format(source, {
2418
...options,
2519
plugins: await getPrettierPlugins(options),
@@ -30,7 +24,7 @@ const formatFile = async (
3024
}
3125

3226
if (shouldWrite) {
33-
await writeFile(path, formatted, atomicWriteOptions);
27+
writeFileSync(path, formatted, 'utf8');
3428
}
3529

3630
return true;

packages/rstack/tests/fmt/runnerWriteFailure.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ const mocks = rs.hoisted(() => ({
88
rs.mock('../../src/fmt/parallel.ts', () => ({
99
createFmtWorker: () =>
1010
Promise.resolve({
11-
formatFile: () => Promise.reject(new Error('atomic write failed')),
11+
formatFile: () => Promise.reject(new Error('file write failed')),
1212
terminate: () => {
1313
mocks.terminateCalls++;
1414
},
1515
}),
1616
}));
1717

18-
test('returns an error when the atomic write fails', async () => {
18+
test('returns an error when a file write fails', async () => {
1919
const filePath = '/virtual/example.ts';
2020

2121
const result = await runFmtFiles({
@@ -38,7 +38,7 @@ test('returns an error when the atomic write fails', async () => {
3838
{
3939
path: filePath,
4040
status: 'error',
41-
error: { message: 'atomic write failed' },
41+
error: { message: 'file write failed' },
4242
},
4343
],
4444
});
Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,25 @@
1-
import { expect, rs, test } from 'rstack/test';
1+
import { readFileSync } from 'node:fs';
2+
import { expect, test } from 'rstack/test';
23
import { formatFile } from '../../src/fmt/worker.ts';
4+
import { withTempProject, writeProjectFile } from './helpers.ts';
35

4-
const mocks = rs.hoisted(() => ({
5-
writeFileCalls: [] as [string, string, unknown][],
6-
}));
6+
test('writes formatted files', async () => {
7+
await withTempProject(async (rootPath) => {
8+
const filePath = writeProjectFile(rootPath, 'example.ts', 'const value=1');
79

8-
rs.mock('atomically', () => ({
9-
readFile: () => Promise.resolve('const value=1'),
10-
writeFile: (path: string, data: string, options: unknown) => {
11-
mocks.writeFileCalls.push([path, data, options]);
12-
return Promise.resolve();
13-
},
14-
}));
15-
16-
test('disables fsync for atomic writes', async () => {
17-
const filePath = '/virtual/example.ts';
18-
19-
await expect(
20-
formatFile(
21-
{
22-
path: filePath,
23-
options: {
24-
filepath: filePath,
25-
parser: 'typescript',
10+
await expect(
11+
formatFile(
12+
{
13+
path: filePath,
14+
options: {
15+
filepath: filePath,
16+
parser: 'typescript',
17+
},
2618
},
27-
},
28-
true,
29-
),
30-
).resolves.toBe(true);
19+
true,
20+
),
21+
).resolves.toBe(true);
3122

32-
expect(mocks.writeFileCalls).toEqual([
33-
[filePath, 'const value = 1;\n', { encoding: 'utf8', fsync: false }],
34-
]);
23+
expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n');
24+
});
3525
});

pnpm-lock.yaml

Lines changed: 0 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ catalog:
3535
'@types/react': '^19.2.18'
3636
'@types/react-dom': '^19.2.4'
3737
'@shikijs/transformers': '^4.3.1'
38-
atomically: '2.1.1'
3938
'cspell-ban-words': '^0.0.4'
4039
'fast-ignore': '2.0.0'
4140
'happy-dom': '^20.11.1'

0 commit comments

Comments
 (0)