Skip to content

Commit 4466dee

Browse files
authored
fs: add c++ fast path for writeFileSync utf8
PR-URL: #49884 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Santiago Gimeno <[email protected]> Reviewed-By: Stephen Belanger <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent f11b206 commit 4466dee

File tree

5 files changed

+156
-0
lines changed

5 files changed

+156
-0
lines changed

benchmark/fs/bench-writeFileSync.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const fs = require('fs');
5+
const tmpdir = require('../../test/common/tmpdir');
6+
tmpdir.refresh();
7+
8+
// Some variants are commented out as they do not show a change and just slow
9+
const bench = common.createBenchmark(main, {
10+
encoding: ['utf8'],
11+
useFd: ['true', 'false'],
12+
length: [1024, 102400, 1024 * 1024],
13+
14+
// useBuffer: ['true', 'false'],
15+
useBuffer: ['false'],
16+
17+
// func: ['appendFile', 'writeFile'],
18+
func: ['writeFile'],
19+
20+
n: [1e3],
21+
});
22+
23+
function main({ n, func, encoding, length, useFd, useBuffer }) {
24+
tmpdir.refresh();
25+
const enc = encoding === 'undefined' ? undefined : encoding;
26+
const path = tmpdir.resolve(`.writefilesync-file-${Date.now()}`);
27+
28+
useFd = useFd === 'true';
29+
const file = useFd ? fs.openSync(path, 'w') : path;
30+
31+
let data = 'a'.repeat(length);
32+
if (useBuffer === 'true') data = Buffer.from(data, encoding);
33+
34+
const fn = fs[func + 'Sync'];
35+
36+
bench.start();
37+
for (let i = 0; i < n; ++i) {
38+
fn(file, data, enc);
39+
}
40+
bench.end(n);
41+
42+
if (useFd) fs.closeSync(file);
43+
}

lib/fs.js

+13
Original file line numberDiff line numberDiff line change
@@ -2343,6 +2343,19 @@ function writeFileSync(path, data, options) {
23432343

23442344
validateBoolean(flush, 'options.flush');
23452345

2346+
// C++ fast path for string data and UTF8 encoding
2347+
if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8')) {
2348+
if (!isInt32(path)) {
2349+
path = pathModule.toNamespacedPath(getValidatedPath(path));
2350+
}
2351+
2352+
return binding.writeFileUtf8(
2353+
path, data,
2354+
stringToFlags(options.flag),
2355+
parseFileMode(options.mode, 'mode', 0o666),
2356+
);
2357+
}
2358+
23462359
if (!isArrayBufferView(data)) {
23472360
validateStringAfterArrayBufferView(data, 'data');
23482361
data = Buffer.from(data, options.encoding || 'utf8');

src/node_file.cc

+80
Original file line numberDiff line numberDiff line change
@@ -2230,6 +2230,84 @@ static void WriteString(const FunctionCallbackInfo<Value>& args) {
22302230
}
22312231
}
22322232

2233+
static void WriteFileUtf8(const FunctionCallbackInfo<Value>& args) {
2234+
// Fast C++ path for fs.writeFileSync(path, data) with utf8 encoding
2235+
// (file, data, options.flag, options.mode)
2236+
2237+
Environment* env = Environment::GetCurrent(args);
2238+
auto isolate = env->isolate();
2239+
2240+
CHECK_EQ(args.Length(), 4);
2241+
2242+
BufferValue value(isolate, args[1]);
2243+
CHECK_NOT_NULL(*value);
2244+
2245+
CHECK(args[2]->IsInt32());
2246+
const int flags = args[2].As<Int32>()->Value();
2247+
2248+
CHECK(args[3]->IsInt32());
2249+
const int mode = args[3].As<Int32>()->Value();
2250+
2251+
uv_file file;
2252+
2253+
bool is_fd = args[0]->IsInt32();
2254+
2255+
// Check for file descriptor
2256+
if (is_fd) {
2257+
file = args[0].As<Int32>()->Value();
2258+
} else {
2259+
BufferValue path(isolate, args[0]);
2260+
CHECK_NOT_NULL(*path);
2261+
if (CheckOpenPermissions(env, path, flags).IsNothing()) return;
2262+
2263+
FSReqWrapSync req_open("open", *path);
2264+
2265+
FS_SYNC_TRACE_BEGIN(open);
2266+
file =
2267+
SyncCallAndThrowOnError(env, &req_open, uv_fs_open, *path, flags, mode);
2268+
FS_SYNC_TRACE_END(open);
2269+
2270+
if (is_uv_error(file)) {
2271+
return;
2272+
}
2273+
}
2274+
2275+
int bytesWritten = 0;
2276+
uint32_t offset = 0;
2277+
2278+
const size_t length = value.length();
2279+
uv_buf_t uvbuf = uv_buf_init(value.out(), length);
2280+
2281+
FS_SYNC_TRACE_BEGIN(write);
2282+
while (offset < length) {
2283+
FSReqWrapSync req_write("write");
2284+
bytesWritten = SyncCallAndThrowOnError(
2285+
env, &req_write, uv_fs_write, file, &uvbuf, 1, -1);
2286+
2287+
// Write errored out
2288+
if (bytesWritten < 0) {
2289+
break;
2290+
}
2291+
2292+
offset += bytesWritten;
2293+
DCHECK_LE(offset, length);
2294+
uvbuf.base += bytesWritten;
2295+
uvbuf.len -= bytesWritten;
2296+
}
2297+
FS_SYNC_TRACE_END(write);
2298+
2299+
if (!is_fd) {
2300+
FSReqWrapSync req_close("close");
2301+
2302+
FS_SYNC_TRACE_BEGIN(close);
2303+
int result = SyncCallAndThrowOnError(env, &req_close, uv_fs_close, file);
2304+
FS_SYNC_TRACE_END(close);
2305+
2306+
if (is_uv_error(result)) {
2307+
return;
2308+
}
2309+
}
2310+
}
22332311

22342312
/*
22352313
* Wrapper for read(2).
@@ -3071,6 +3149,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
30713149
SetMethod(isolate, target, "writeBuffer", WriteBuffer);
30723150
SetMethod(isolate, target, "writeBuffers", WriteBuffers);
30733151
SetMethod(isolate, target, "writeString", WriteString);
3152+
SetMethod(isolate, target, "writeFileUtf8", WriteFileUtf8);
30743153
SetMethod(isolate, target, "realpath", RealPath);
30753154
SetMethod(isolate, target, "copyFile", CopyFile);
30763155

@@ -3190,6 +3269,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
31903269
registry->Register(WriteBuffer);
31913270
registry->Register(WriteBuffers);
31923271
registry->Register(WriteString);
3272+
registry->Register(WriteFileUtf8);
31933273
registry->Register(RealPath);
31943274
registry->Register(CopyFile);
31953275

test/parallel/test-fs-sync-fd-leak.js

+17
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,34 @@ fs.writeSync = function() {
4141
throw new Error('BAM');
4242
};
4343

44+
// Internal fast paths are pure C++, can't error inside write
45+
internalBinding('fs').writeFileUtf8 = function() {
46+
// Fake close
47+
close_called++;
48+
throw new Error('BAM');
49+
};
50+
4451
internalBinding('fs').fstat = function() {
4552
throw new Error('EBADF: bad file descriptor, fstat');
4653
};
4754

4855
let close_called = 0;
4956
ensureThrows(function() {
57+
// Fast path: writeFileSync utf8
5058
fs.writeFileSync('dummy', 'xxx');
5159
}, 'BAM');
5260
ensureThrows(function() {
61+
// Non-fast path
62+
fs.writeFileSync('dummy', 'xxx', { encoding: 'base64' });
63+
}, 'BAM');
64+
ensureThrows(function() {
65+
// Fast path: writeFileSync utf8
5366
fs.appendFileSync('dummy', 'xxx');
5467
}, 'BAM');
68+
ensureThrows(function() {
69+
// Non-fast path
70+
fs.appendFileSync('dummy', 'xxx', { encoding: 'base64' });
71+
}, 'BAM');
5572

5673
function ensureThrows(cb, message) {
5774
let got_exception = false;

typings/internalBinding/fs.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ declare namespace InternalFSBinding {
230230
function writeString(fd: number, value: string, pos: unknown, encoding: unknown, usePromises: typeof kUsePromises): Promise<number>;
231231

232232
function getFormatOfExtensionlessFile(url: string): ConstantsBinding['fs'];
233+
234+
function writeFileUtf8(path: string, data: string, flag: number, mode: number): void;
235+
function writeFileUtf8(fd: number, data: string, flag: number, mode: number): void;
233236
}
234237

235238
export interface FsBinding {

0 commit comments

Comments
 (0)