diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c5c34f13..95086aba2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: cd uWebSockets.js ${{ matrix.os == 'windows-latest' && 'nmake' || 'make' }} ls dist - cd tests && npm install ws && node smoke.js && cd .. + cd tests && npm install ws && node smoke.js && node --expose-gc strings.js && cd .. ls dist git fetch origin binaries:binaries git checkout binaries diff --git a/src/HttpRequestWrapper.h b/src/HttpRequestWrapper.h index 7470abcb0..e30305a4b 100644 --- a/src/HttpRequestWrapper.h +++ b/src/HttpRequestWrapper.h @@ -50,7 +50,9 @@ struct HttpRequestWrapper { Local cb = Local::Cast(args[0]); for (auto p : *req) { - Local argv[] = {String::NewFromUtf8(isolate, p.first.data(), NewStringType::kNormal, p.first.length()).ToLocalChecked(), + /* Keys repeat on every request, kInternalized hits V8's string table + * instead of allocating, and is already interned for JS property use */ + Local argv[] = {String::NewFromUtf8(isolate, p.first.data(), NewStringType::kInternalized, p.first.length()).ToLocalChecked(), String::NewFromUtf8(isolate, p.second.data(), NewStringType::kNormal, p.second.length()).ToLocalChecked()}; /* This one is also called from JS so no need for CallJS */ cb->Call(isolate->GetCurrentContext(), isolate->GetCurrentContext()->Global(), 2, argv).IsEmpty(); @@ -133,7 +135,8 @@ struct HttpRequestWrapper { if (req) { std::string_view method = req->getMethod(); - args.GetReturnValue().Set(String::NewFromUtf8(isolate, method.data(), NewStringType::kNormal, method.length()).ToLocalChecked()); + /* Drawn from a fixed small set, kInternalized dedupes via V8's string table */ + args.GetReturnValue().Set(String::NewFromUtf8(isolate, method.data(), NewStringType::kInternalized, method.length()).ToLocalChecked()); } } @@ -145,7 +148,7 @@ struct HttpRequestWrapper { if (req) { std::string_view method = req->getCaseSensitiveMethod(); - args.GetReturnValue().Set(String::NewFromUtf8(isolate, method.data(), NewStringType::kNormal, method.length()).ToLocalChecked()); + args.GetReturnValue().Set(String::NewFromUtf8(isolate, method.data(), NewStringType::kInternalized, method.length()).ToLocalChecked()); } } diff --git a/src/HttpResponseWrapper.h b/src/HttpResponseWrapper.h index df01ab61d..3fe6b45c7 100644 --- a/src/HttpResponseWrapper.h +++ b/src/HttpResponseWrapper.h @@ -94,8 +94,8 @@ struct HttpResponseWrapper { Isolate *isolate = args.GetIsolate(); auto *res = getHttpResponse(args); if (res) { - /* This thing perfectly fits in with unique_function, and will Reset on destructor */ - UniquePersistent p(isolate, Local::Cast(args[0])); + /* Noexcept move keeps the 16 byte capture in MoveOnlyFunction's inline storage */ + NoexceptPersistent p(isolate, Local::Cast(args[0])); res->onData([p = std::move(p), isolate](std::string_view data, bool last) { HandleScope hs(isolate); @@ -103,7 +103,7 @@ struct HttpResponseWrapper { Local dataArrayBuffer = ArrayBuffer_New(isolate, (void *) data.data(), data.length()); Local argv[] = {dataArrayBuffer, Boolean::New(isolate, last)}; - CallJS(isolate, Local::New(isolate, p), 2, argv); + CallJS(isolate, Local::New(isolate, p.p), 2, argv); dataArrayBuffer->Detach(); }); @@ -197,8 +197,8 @@ struct HttpResponseWrapper { Isolate *isolate = args.GetIsolate(); auto *res = getHttpResponse(args); if (res) { - /* This thing perfectly fits in with unique_function, and will Reset on destructor */ - UniquePersistent p(isolate, Local::Cast(args[0])); + /* Noexcept move keeps the 16 byte capture in MoveOnlyFunction's inline storage */ + NoexceptPersistent p(isolate, Local::Cast(args[0])); res->onDataV2([p = std::move(p), isolate](std::string_view data, uint64_t maxRemainingBodyLength) { HandleScope hs(isolate); @@ -207,7 +207,7 @@ struct HttpResponseWrapper { /* Pass maxRemainingBodyLength so user can preallocate; 0 signals the last chunk */ Local argv[] = {dataArrayBuffer, BigInt::NewFromUnsigned(isolate, maxRemainingBodyLength)}; - CallJS(isolate, Local::New(isolate, p), 2, argv); + CallJS(isolate, Local::New(isolate, p.p), 2, argv); dataArrayBuffer->Detach(); }); @@ -222,19 +222,22 @@ struct HttpResponseWrapper { Isolate *isolate = args.GetIsolate(); auto *res = getHttpResponse(args); if (res) { - /* This thing perfectly fits in with unique_function, and will Reset on destructor */ - UniquePersistent p(isolate, Local::Cast(args[0])); + /* The callback is kept alive by the res object itself (internal field 1): + * one global handle per request instead of two, and a 16 byte capture that + * fits MoveOnlyFunction's inline storage instead of heap-allocating */ + args.This()->SetInternalField(1, args[0]); - /* This is how we capture res (C++ this in invocation of this function) */ - UniquePersistent resObject(isolate, args.This()); + NoexceptPersistent resObject(isolate, args.This()); - res->onAborted([p = std::move(p), resObject = std::move(resObject), isolate]() { + res->onAborted([resObject = std::move(resObject), isolate]() { HandleScope hs(isolate); + Local resLocal = Local::New(isolate, resObject.p); + /* Mark this resObject invalid */ - setInternalPointer(Local::New(isolate, resObject), nullptr);//->SetAlignedPointerInInternalField(0, nullptr); + setInternalPointer(resLocal, nullptr); - CallJS(isolate, Local::New(isolate, p), 0, nullptr); + CallJS(isolate, Local::Cast(resLocal->GetInternalField(1).As()), 0, nullptr); }); args.GetReturnValue().Set(args.This()); @@ -341,8 +344,8 @@ struct HttpResponseWrapper { Isolate *isolate = args.GetIsolate(); auto *res = getHttpResponse(args); if (res) { - /* This thing perfectly fits in with unique_function, and will Reset on destructor */ - UniquePersistent p(isolate, Local::Cast(args[0])); + /* Noexcept move keeps the 16 byte capture in MoveOnlyFunction's inline storage */ + NoexceptPersistent p(isolate, Local::Cast(args[0])); res->onWritable([p = std::move(p), isolate](size_t offset) -> bool { HandleScope hs(isolate); @@ -350,7 +353,7 @@ struct HttpResponseWrapper { Local argv[] = {Number::New(isolate, offset)}; /* We should check if this is really here! */ - MaybeLocal maybeBoolean = CallJS(isolate, Local::New(isolate, p), 1, argv); + MaybeLocal maybeBoolean = CallJS(isolate, Local::New(isolate, p.p), 1, argv); if (maybeBoolean.IsEmpty()) { std::cerr << "Warning: uWS.HttpResponse.onWritable callback should return Boolean. See documentation for uWS.HttpResponse.onWritable and consult the user manual." << std::endl; /* The default should be true, as it only adds a potential extra send, rather than erroneously avoid it */ @@ -454,11 +457,8 @@ struct HttpResponseWrapper { } /* This is a quick fix, it will need updating in µWS later on */ - Local array = Array::New(isolate, 2); - array->Set(isolate->GetCurrentContext(), 0, Boolean::New(isolate, ok)).ToChecked(); - array->Set(isolate->GetCurrentContext(), 1, Boolean::New(isolate, hasResponded)).ToChecked(); - - args.GetReturnValue().Set(array); + Local elems[] = {Boolean::New(isolate, ok), Boolean::New(isolate, hasResponded)}; + args.GetReturnValue().Set(Array::New(isolate, elems, 2)); } } @@ -591,7 +591,8 @@ struct HttpResponseWrapper { } else if (SSL == 3) { resTemplateLocal->SetClassName(String::NewFromUtf8(isolate, "uWS.CachedHttpResponse", NewStringType::kNormal).ToLocalChecked()); } - resTemplateLocal->InstanceTemplate()->SetInternalFieldCount(1); + /* Field 0 is the native pointer, field 1 holds the onAborted callback */ + resTemplateLocal->InstanceTemplate()->SetInternalFieldCount(2); /* Register our functions */ resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "end", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_end)); diff --git a/src/Utilities.h b/src/Utilities.h index 78ce93845..530f6a1cb 100644 --- a/src/Utilities.h +++ b/src/Utilities.h @@ -112,6 +112,16 @@ static inline bool missingArguments(int length, const FunctionCallbackInfo +struct NoexceptPersistent { + UniquePersistent p; + NoexceptPersistent(Isolate *isolate, const Local &v) : p(isolate, v) {} + NoexceptPersistent(NoexceptPersistent &&other) noexcept : p(std::move(other.p)) {} +}; + struct Callback { bool invalid = false; UniquePersistent f; @@ -185,6 +195,30 @@ class NativeString { } else if (value->IsString()) { Local string = Local::Cast(value); +#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 5) + /* One-byte ASCII fast path: single scan + memcpy instead of the two-pass + * Utf8Length + WriteUtf8 (ASCII bytes are identical in Latin-1 and Utf-8). + * No V8 allocation may happen while the view is alive, we only copy */ + { + String::ValueView view(isolate, string); + if (view.is_one_byte()) { + const uint8_t *src = view.data8(); + uint32_t len = view.length(); + uint8_t seen = 0; + for (uint32_t i = 0; i < len; i++) { + seen |= src[i]; + } + if (!(seen & 0x80)) { + length = len; + data = alloc(len); + allocated = true; + memcpy(data, src, len); + return; + } + } + } +#endif + /* StringView path is Latin-1, not Utf-8 */ #if (V8_MAJOR_VERSION == 14) diff --git a/src/WebSocketWrapper.h b/src/WebSocketWrapper.h index 07a45d305..5f071ecc2 100644 --- a/src/WebSocketWrapper.h +++ b/src/WebSocketWrapper.h @@ -19,7 +19,10 @@ #include "Utilities.h" #include +/* V8 14 (Node 26) removed the fast api calls header */ +#if V8_MAJOR_VERSION < 14 #include "v8-fast-api-calls.h" +#endif using namespace v8; /* todo: probably isCorked, cork should be exposed? */ @@ -331,6 +334,7 @@ struct WebSocketWrapper { * A null internal-field pointer (closed socket) sets options.fallback = true so V8 * re-invokes the slow path which throws the proper exception. */ +#if V8_MAJOR_VERSION < 14 // Version A: Handles Strings template static uint32_t uWS_WebSocket_send_fast_string(v8::Local receiver, @@ -369,6 +373,7 @@ static uint32_t uWS_WebSocket_send_fast_buffer(v8::Local receiver, return ws->send(std::string_view(data, length), isBinary ? uWS::OpCode::BINARY : uWS::OpCode::TEXT, compress); } +#endif template static Local init(Isolate *isolate) { @@ -386,7 +391,9 @@ static uint32_t uWS_WebSocket_send_fast_buffer(v8::Local receiver, wsTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "sendLastFragment", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, uWS_WebSocket_sendLastFragment)); wsTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "getUserData", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, uWS_WebSocket_getUserData)); +#if V8_MAJOR_VERSION < 14 static v8::CFunction fast_send = v8::CFunction::Make(uWS_WebSocket_send_fast_buffer); +#endif wsTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "send", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, uWS_WebSocket_send/*, Local(), Local(), 0, ConstructorBehavior::kThrow, SideEffectType::kHasSideEffect, &fast_send*/)); wsTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "end", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, uWS_WebSocket_end)); wsTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "close", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, uWS_WebSocket_close)); diff --git a/tests/strings.js b/tests/strings.js new file mode 100644 index 000000000..f49b1b67d Binary files /dev/null and b/tests/strings.js differ