Skip to content

Commit ba6a523

Browse files
committed
stream: create write request objects lazily
Every stream write created a WriteWrap JS object up front, even though most writes complete synchronously via uv_try_write() and never use it. Let stream_base_commons pass null instead of a request object. StreamBase::Write() already creates the wrap object only when the write does not complete synchronously; return that object to JS (which attaches oncomplete/callback to it) and a plain error code otherwise. Writes that complete synchronously now cross the JS/C++ boundary once and allocate nothing. Callers that pass in a request object (child_process IPC, webstreams adapters) behave as before. Since Http2Stream::DoWrite() can invoke the completion callback synchronously - before JS has attached oncomplete - such completions are now recorded on the request object's writeStatus field and replayed by stream_base_commons after dispatch. This also replaces a Has() plus name-based MakeCallback() pair with a single Get(). Also pre-create the JS fields of WriteWrap instances in the object template, as was already done for ShutdownWrap, so that they are in-object properties. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent e6736e6 commit ba6a523

5 files changed

Lines changed: 185 additions & 87 deletions

File tree

lib/internal/stream_base_commons.js

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ const {
88
const { Buffer } = require('buffer');
99
const { FastBuffer } = require('internal/buffer');
1010
const {
11-
WriteWrap,
1211
kReadBytesOrError,
1312
kArrayBufferOffset,
1413
kBytesWritten,
15-
kLastWriteWasAsync,
14+
kLastWriteErr,
1615
streamBaseState,
1716
} = internalBinding('stream_wrap');
1817
const { UV_EOF } = internalBinding('uv');
@@ -43,41 +42,6 @@ const kBuffer = Symbol('kBuffer');
4342
const kBufferGen = Symbol('kBufferGen');
4443
const kBufferCb = Symbol('kBufferCb');
4544

46-
function handleWriteReq(req, data, encoding) {
47-
const { handle } = req;
48-
49-
switch (encoding) {
50-
case 'buffer':
51-
{
52-
const ret = handle.writeBuffer(req, data);
53-
if (streamBaseState[kLastWriteWasAsync])
54-
req.buffer = data;
55-
return ret;
56-
}
57-
case 'latin1':
58-
case 'binary':
59-
return handle.writeLatin1String(req, data);
60-
case 'utf8':
61-
case 'utf-8':
62-
return handle.writeUtf8String(req, data);
63-
case 'ascii':
64-
return handle.writeAsciiString(req, data);
65-
case 'ucs2':
66-
case 'ucs-2':
67-
case 'utf16le':
68-
case 'utf-16le':
69-
return handle.writeUcs2String(req, data);
70-
default:
71-
{
72-
const buffer = Buffer.from(data, encoding);
73-
const ret = handle.writeBuffer(req, buffer);
74-
if (streamBaseState[kLastWriteWasAsync])
75-
req.buffer = buffer;
76-
return ret;
77-
}
78-
}
79-
}
80-
8145
function onWriteComplete(status) {
8246
debug('onWriteComplete', status, this.error);
8347

@@ -105,21 +69,8 @@ function onWriteComplete(status) {
10569
this.callback(null);
10670
}
10771

108-
function createWriteWrap(handle, callback) {
109-
const req = new WriteWrap();
110-
111-
req.handle = handle;
112-
req.oncomplete = onWriteComplete;
113-
req.async = false;
114-
req.bytes = 0;
115-
req.buffer = null;
116-
req.callback = callback;
117-
118-
return req;
119-
}
120-
12172
function writevGeneric(self, data, cb) {
122-
const req = createWriteWrap(self[kHandle], cb);
73+
const handle = self[kHandle];
12374
const allBuffers = data.allBuffers;
12475
let chunks;
12576
if (allBuffers) {
@@ -134,33 +85,87 @@ function writevGeneric(self, data, cb) {
13485
chunks[i * 2 + 1] = entry.encoding;
13586
}
13687
}
137-
const err = req.handle.writev(req, chunks, allBuffers);
138-
139-
// Retain chunks
140-
if (err === 0) req._chunks = chunks;
88+
const ret = handle.writev(null, chunks, allBuffers);
14189

142-
afterWriteDispatched(req, err, cb);
143-
return req;
90+
return afterWriteDispatched(handle, ret, chunks, cb);
14491
}
14592

14693
function writeGeneric(self, data, encoding, cb) {
147-
const req = createWriteWrap(self[kHandle], cb);
148-
const err = handleWriteReq(req, data, encoding);
94+
const handle = self[kHandle];
95+
let ret;
96+
let buffer = null;
14997

150-
afterWriteDispatched(req, err, cb);
151-
return req;
98+
switch (encoding) {
99+
case 'buffer':
100+
buffer = data;
101+
ret = handle.writeBuffer(null, data);
102+
break;
103+
case 'latin1':
104+
case 'binary':
105+
ret = handle.writeLatin1String(null, data);
106+
break;
107+
case 'utf8':
108+
case 'utf-8':
109+
ret = handle.writeUtf8String(null, data);
110+
break;
111+
case 'ascii':
112+
ret = handle.writeAsciiString(null, data);
113+
break;
114+
case 'ucs2':
115+
case 'ucs-2':
116+
case 'utf16le':
117+
case 'utf-16le':
118+
ret = handle.writeUcs2String(null, data);
119+
break;
120+
default:
121+
buffer = Buffer.from(data, encoding);
122+
ret = handle.writeBuffer(null, buffer);
123+
break;
124+
}
125+
126+
return afterWriteDispatched(handle, ret, buffer, cb);
152127
}
153128

154-
function afterWriteDispatched(req, err, cb) {
155-
req.bytes = streamBaseState[kBytesWritten];
156-
req.async = !!streamBaseState[kLastWriteWasAsync];
129+
// `ret` is either a numeric error code (when the write - or its failure -
130+
// completed synchronously and no write request was created) or the WriteWrap
131+
// object of a dispatched write.
132+
function afterWriteDispatched(handle, ret, buffer, cb) {
133+
const bytes = streamBaseState[kBytesWritten];
134+
135+
if (typeof ret === 'number') {
136+
// The write (or its failure) completed synchronously.
137+
if (ret !== 0)
138+
cb(new ErrnoException(ret, 'write'));
139+
else if (typeof cb === 'function')
140+
cb();
141+
return { async: false, bytes };
142+
}
157143

158-
if (err !== 0)
159-
return cb(new ErrnoException(err, 'write', req.error));
144+
const req = ret;
145+
const err = streamBaseState[kLastWriteErr];
146+
if (err !== 0) {
147+
cb(new ErrnoException(err, 'write', req.error));
148+
return { async: false, bytes };
149+
}
160150

161-
if (!req.async && typeof req.callback === 'function') {
162-
req.callback();
151+
req.handle = handle;
152+
req.oncomplete = onWriteComplete;
153+
req.callback = cb;
154+
req.async = true;
155+
req.bytes = bytes;
156+
// Retain the data (or chunks) being written until the write completes.
157+
req.buffer = buffer;
158+
159+
// If the write completed synchronously inside the write call, before
160+
// `oncomplete` could be attached, the completion status has been recorded;
161+
// deliver it now.
162+
const status = req.writeStatus;
163+
if (status !== null) {
164+
req.writeStatus = null;
165+
req.oncomplete(status);
163166
}
167+
168+
return req;
164169
}
165170

166171
function onStreamRead(arrayBuffer) {

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@
404404
V(writable_string, "writable") \
405405
V(write_host_object_string, "_writeHostObject") \
406406
V(write_queue_size_string, "writeQueueSize") \
407+
V(write_status_string, "writeStatus") \
407408
V(zlib_string, "zlib") \
408409
V(zstd_string, "zstd")
409410

src/stream_base.cc

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,37 @@ int StreamBase::Shutdown(const FunctionCallbackInfo<Value>& args) {
175175
void StreamBase::SetWriteResult(const StreamWriteResult& res) {
176176
env_->stream_base_state()[kBytesWritten] = res.bytes;
177177
env_->stream_base_state()[kLastWriteWasAsync] = res.async;
178+
env_->stream_base_state()[kLastWriteErr] = res.err;
179+
}
180+
181+
// Finish a JS-initiated write. When the caller did not pass a request
182+
// object (`lazy_req`), the write wrap object exists only if the write did
183+
// not complete synchronously; hand it to JS so that it can attach its
184+
// completion callback. Otherwise the numeric error code (via JSMethod) is
185+
// all JS needs.
186+
int StreamBase::FinishWrite(const v8::FunctionCallbackInfo<v8::Value>& args,
187+
const StreamWriteResult& res,
188+
bool lazy_req) {
189+
SetWriteResult(res);
190+
if (lazy_req && res.wrap_obj) {
191+
args.GetReturnValue().Set(res.wrap_obj->object());
192+
return kReturnValueSet;
193+
}
194+
return res.err;
178195
}
179196

180197
int StreamBase::Writev(const FunctionCallbackInfo<Value>& args) {
181198
Environment* env = Environment::GetCurrent(args);
182199
Isolate* isolate = env->isolate();
183200
Local<Context> context = env->context();
184201

185-
CHECK(args[0]->IsObject());
186202
CHECK(args[1]->IsArray());
187203

188-
Local<Object> req_wrap_obj = args[0].As<Object>();
204+
// When no request object is passed in, one is created by Write() only if
205+
// the write does not complete synchronously; see FinishWrite().
206+
const bool lazy_req = !args[0]->IsObject();
207+
Local<Object> req_wrap_obj;
208+
if (!lazy_req) req_wrap_obj = args[0].As<Object>();
189209
Local<Array> chunks = args[1].As<Array>();
190210
bool all_buffers = args[2]->IsTrue();
191211

@@ -287,20 +307,19 @@ int StreamBase::Writev(const FunctionCallbackInfo<Value>& args) {
287307
}
288308

289309
StreamWriteResult res = Write(*bufs, count, nullptr, req_wrap_obj);
290-
SetWriteResult(res);
291310
if (res.wrap != nullptr && storage_size > 0)
292311
res.wrap->SetBackingStore(std::move(bs));
293-
return res.err;
312+
return FinishWrite(args, res, lazy_req);
294313
}
295314

296-
297315
int StreamBase::WriteBuffer(const FunctionCallbackInfo<Value>& args) {
298-
CHECK(args[0]->IsObject());
299316
CHECK(args[1]->IsUint8Array());
300317

301318
Environment* env = Environment::GetCurrent(args);
302319

303-
Local<Object> req_wrap_obj = args[0].As<Object>();
320+
bool lazy_req = !args[0]->IsObject();
321+
Local<Object> req_wrap_obj;
322+
if (!lazy_req) req_wrap_obj = args[0].As<Object>();
304323
uv_buf_t buf;
305324
buf.base = Buffer::Data(args[1]);
306325
buf.len = Buffer::Length(args[1]);
@@ -310,6 +329,16 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo<Value>& args) {
310329
if (args[2]->IsObject() && IsIPCPipe()) {
311330
Local<Object> send_handle_obj = args[2].As<Object>();
312331

332+
if (lazy_req) {
333+
// Sending a handle requires a request object up front to reference it.
334+
if (!env->write_wrap_template()
335+
->NewInstance(env->context())
336+
.ToLocal(&req_wrap_obj)) {
337+
return UV_EBUSY;
338+
}
339+
StreamReq::ResetObject(req_wrap_obj);
340+
}
341+
313342
HandleWrap* wrap;
314343
ASSIGN_OR_RETURN_UNWRAP(&wrap, send_handle_obj, UV_EINVAL);
315344
send_handle = reinterpret_cast<uv_stream_t*>(wrap->GetHandle());
@@ -323,20 +352,18 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo<Value>& args) {
323352
}
324353

325354
StreamWriteResult res = Write(&buf, 1, send_handle, req_wrap_obj);
326-
SetWriteResult(res);
327-
328-
return res.err;
355+
return FinishWrite(args, res, lazy_req);
329356
}
330357

331-
332358
template <enum encoding enc>
333359
int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args) {
334360
Environment* env = Environment::GetCurrent(args);
335361
Isolate* isolate = env->isolate();
336-
CHECK(args[0]->IsObject());
337362
CHECK(args[1]->IsString());
338363

339-
Local<Object> req_wrap_obj = args[0].As<Object>();
364+
const bool lazy_req = !args[0]->IsObject();
365+
Local<Object> req_wrap_obj;
366+
if (!lazy_req) req_wrap_obj = args[0].As<Object>();
340367
Local<String> string = args[1].As<String>();
341368
Local<Object> send_handle_obj;
342369
if (args[2]->IsObject())
@@ -417,6 +444,16 @@ int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args) {
417444
uv_stream_t* send_handle = nullptr;
418445

419446
if (IsIPCPipe() && !send_handle_obj.IsEmpty()) {
447+
if (lazy_req && req_wrap_obj.IsEmpty()) {
448+
// Sending a handle requires a request object up front to reference it.
449+
if (!env->write_wrap_template()
450+
->NewInstance(env->context())
451+
.ToLocal(&req_wrap_obj)) {
452+
return UV_EBUSY;
453+
}
454+
StreamReq::ResetObject(req_wrap_obj);
455+
}
456+
420457
HandleWrap* wrap;
421458
ASSIGN_OR_RETURN_UNWRAP(&wrap, send_handle_obj, UV_EINVAL);
422459
send_handle = reinterpret_cast<uv_stream_t*>(wrap->GetHandle());
@@ -432,11 +469,10 @@ int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args) {
432469
StreamWriteResult res = Write(&buf, 1, send_handle, req_wrap_obj, try_write);
433470
res.bytes += synchronously_written;
434471

435-
SetWriteResult(res);
436472
if (res.wrap != nullptr)
437473
res.wrap->SetBackingStore(std::move(bs));
438474

439-
return res.err;
475+
return FinishWrite(args, res, lazy_req);
440476
}
441477

442478

@@ -662,7 +698,8 @@ void StreamBase::JSMethod(const FunctionCallbackInfo<Value>& args) {
662698
if (!wrap->IsAlive()) return args.GetReturnValue().Set(UV_EINVAL);
663699

664700
AsyncHooks::DefaultTriggerAsyncIdScope trigger_scope(wrap->GetAsyncWrap());
665-
args.GetReturnValue().Set((wrap->*Method)(args));
701+
int ret = (wrap->*Method)(args);
702+
if (ret != kReturnValueSet) args.GetReturnValue().Set(ret);
666703
}
667704

668705
int StreamResource::DoTryWrite(uv_buf_t** bufs, size_t* count) {
@@ -773,20 +810,50 @@ void ReportWritesToJSStreamListener::OnStreamAfterReqFinished(
773810
CHECK(!async_wrap->persistent().IsEmpty());
774811
Local<Object> req_wrap_obj = async_wrap->object();
775812

813+
Local<Value> oncomplete;
814+
if (!req_wrap_obj->Get(env->context(), env->oncomplete_string())
815+
.ToLocal(&oncomplete)) {
816+
return;
817+
}
818+
819+
const char* msg = stream->Error();
820+
821+
if (!oncomplete->IsFunction()) {
822+
// The completion callback has not been attached yet: the write finished
823+
// synchronously inside the JS write call, before the request object was
824+
// returned to JS. Record the status so that JS can replay the callback.
825+
if (req_wrap_obj
826+
->Set(env->context(),
827+
env->write_status_string(),
828+
Integer::New(env->isolate(), status))
829+
.IsNothing()) {
830+
return;
831+
}
832+
if (msg != nullptr) {
833+
if (req_wrap_obj
834+
->Set(env->context(),
835+
env->error_string(),
836+
OneByteString(env->isolate(), msg))
837+
.IsNothing()) {
838+
return;
839+
}
840+
stream->ClearError();
841+
}
842+
return;
843+
}
844+
776845
Local<Value> argv[] = {
777846
Integer::New(env->isolate(), status),
778847
stream->GetObject(),
779848
Undefined(env->isolate())
780849
};
781850

782-
const char* msg = stream->Error();
783851
if (msg != nullptr) {
784852
argv[2] = OneByteString(env->isolate(), msg);
785853
stream->ClearError();
786854
}
787855

788-
if (req_wrap_obj->Has(env->context(), env->oncomplete_string()).FromJust())
789-
async_wrap->MakeCallback(env->oncomplete_string(), arraysize(argv), argv);
856+
async_wrap->MakeCallback(oncomplete.As<Function>(), arraysize(argv), argv);
790857
}
791858

792859
void ReportWritesToJSStreamListener::OnStreamAfterWrite(

0 commit comments

Comments
 (0)