Skip to content

Commit 67f5efd

Browse files
committed
fs: add c++ fast path for writeFileSync utf8
1 parent ea88a3e commit 67f5efd

5 files changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,6 +2232,79 @@ static void WriteString(const FunctionCallbackInfo<Value>& args) {
22322232
}
22332233
}
22342234

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

22362309
/*
22372310
* Wrapper for read(2).
@@ -3073,6 +3146,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
30733146
SetMethod(isolate, target, "writeBuffer", WriteBuffer);
30743147
SetMethod(isolate, target, "writeBuffers", WriteBuffers);
30753148
SetMethod(isolate, target, "writeString", WriteString);
3149+
SetMethod(isolate, target, "writeFileUtf8", WriteFileUtf8);
30763150
SetMethod(isolate, target, "realpath", RealPath);
30773151
SetMethod(isolate, target, "copyFile", CopyFile);
30783152

@@ -3192,6 +3266,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
31923266
registry->Register(WriteBuffer);
31933267
registry->Register(WriteBuffers);
31943268
registry->Register(WriteString);
3269+
registry->Register(WriteFileUtf8);
31953270
registry->Register(RealPath);
31963271
registry->Register(CopyFile);
31973272

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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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)