]> git.kaiwu.me - njs.git/commitdiff
Modules: optimized memory consumption while streaming in qjs.
authorDmitry Volyntsev <xeioex@nginx.com>
Tue, 19 Aug 2025 04:21:09 +0000 (21:21 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Wed, 27 Aug 2025 01:36:30 +0000 (18:36 -0700)
This allows to stream long tcp streams or large http response bodies
with low memory consumption.

This works only for qjs engine, because njs has no GC.

This fixes #943 issue on Github.

nginx/ngx_http_js_module.c
nginx/ngx_stream_js_module.c

index 487c8115dcca2fe5b7ba9ba7047eb315abe5a2af..3f38f86f79e3f816982d954c6b9ed9007aa9fd93 100644 (file)
@@ -5601,10 +5601,12 @@ static JSValue
 ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv)
 {
+    size_t               byte_offset, byte_length, len;
     unsigned             last_buf, flush;
-    JSValue              flags, value;
+    JSValue              flags, value, val, buf;
     ngx_str_t            buffer;
     ngx_buf_t           *b;
+    const char          *str;
     ngx_chain_t         *cl;
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
@@ -5620,10 +5622,6 @@ ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val,
         return JS_ThrowTypeError(cx, "cannot send buffer while not filtering");
     }
 
-    if (ngx_qjs_string(cx, argv[0], &buffer) != NGX_OK) {
-        return JS_ThrowTypeError(cx, "failed get buffer arg");
-    }
-
     flush = ctx->buf->flush;
     last_buf = ctx->buf->last_buf;
 
@@ -5647,29 +5645,106 @@ ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val,
         JS_FreeValue(cx, value);
     }
 
-    cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
-    if (cl == NULL) {
-        return JS_ThrowOutOfMemory(cx);
+    val = argv[0];
+
+    if (JS_IsNullOrUndefined(val)) {
+        buffer.len = 0;
+        buffer.data = NULL;
     }
 
-    b = cl->buf;
+    str = NULL;
+    buf = JS_UNDEFINED;
 
-    b->flush = flush;
-    b->last_buf = last_buf;
+    if (JS_IsString(val)) {
+        goto string;
+    }
 
-    b->memory = (buffer.len ? 1 : 0);
-    b->sync = (buffer.len ? 0 : 1);
-    b->tag = (ngx_buf_tag_t) &ngx_http_js_module;
+    buf = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL);
+    if (!JS_IsException(buf)) {
+        buffer.data = JS_GetArrayBuffer(cx, &buffer.len, buf);
+        if (buffer.data == NULL) {
+            JS_FreeValue(cx, buf);
+            return JS_EXCEPTION;
+        }
 
-    b->start = buffer.data;
-    b->end = buffer.data + buffer.len;
-    b->pos = b->start;
-    b->last = b->end;
+        buffer.data += byte_offset;
+        buffer.len = byte_length;
 
-    *ctx->last_out = cl;
-    ctx->last_out = &cl->next;
+    } else {
+string:
+
+        str = JS_ToCStringLen(cx, &buffer.len, val);
+        if (str == NULL) {
+            return JS_EXCEPTION;
+        }
+
+        buffer.data = (u_char *) str;
+    }
+
+    do {
+        cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
+        if (cl == NULL) {
+            goto out_of_memory;
+        }
+
+        b = cl->buf;
+
+        if (b->start == NULL) {
+            b->start = ngx_pnalloc(r->pool, buffer.len);
+            if (b->start == NULL) {
+                goto out_of_memory;
+            }
+
+            len = buffer.len;
+            b->end = b->start + len;
+
+        } else {
+            len = ngx_min(buffer.len, (size_t) (b->end - b->start));
+        }
+
+        memcpy(b->start, buffer.data, len);
+
+        b->pos = b->start;
+        b->last = b->start + len;
+
+        if (buffer.len == len) {
+            b->last_buf = last_buf;
+            b->flush = flush;
+
+        } else {
+            b->last_buf = 0;
+            b->flush = 0;
+        }
+
+        b->memory = (len ? 1 : 0);
+        b->sync = (len ? 0 : 1);
+        b->tag = (ngx_buf_tag_t) &ngx_http_js_module;
+
+        buffer.data += len;
+        buffer.len -= len;
+
+        *ctx->last_out = cl;
+        ctx->last_out = &cl->next;
+
+    } while (buffer.len != 0);
+
+    if (str != NULL) {
+        JS_FreeCString(cx, str);
+    }
+
+    JS_FreeValue(cx, buf);
 
     return JS_UNDEFINED;
+
+out_of_memory:
+
+    if (str != NULL) {
+        JS_FreeCString(cx, str);
+    }
+
+    JS_FreeValue(cx, buf);
+
+    return JS_ThrowOutOfMemory(cx);
 }
 
 
index a7446c49b7d684ec3b153e7b3e1e125a2523ead3..13b685e52cc4802eaed98a31c3d070e68e6557a2 100644 (file)
@@ -2273,10 +2273,12 @@ static JSValue
 ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc,
     JSValueConst *argv, int from_upstream)
 {
-    JSValue                val;
+    size_t                 byte_offset, byte_length, len;
+    JSValue                val, buf;
     unsigned               last_buf, flush;
     ngx_str_t              buffer;
     ngx_buf_t             *b;
+    const char            *str;
     ngx_chain_t           *cl;
     ngx_connection_t      *c;
     ngx_stream_js_ctx_t   *ctx;
@@ -2295,10 +2297,6 @@ ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc,
         return JS_ThrowInternalError(cx, "cannot send buffer in this handler");
     }
 
-    if (ngx_qjs_string(cx, argv[0], &buffer) != NGX_OK) {
-        return JS_EXCEPTION;
-    }
-
     /*
      * ctx->buf != NULL when s.send() is called while processing incoming
      * data chunks, otherwise s.send() is called asynchronously
@@ -2353,39 +2351,121 @@ ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc,
         }
     }
 
-    cl = ngx_chain_get_free_buf(c->pool, &ctx->free);
-    if (cl == NULL) {
-        return JS_ThrowInternalError(cx, "memory error");
+    val = argv[0];
+
+    if (JS_IsNullOrUndefined(val)) {
+        buffer.len = 0;
+        buffer.data = NULL;
     }
 
-    b = cl->buf;
+    str = NULL;
+    buf = JS_UNDEFINED;
 
-    b->flush = flush;
-    b->last_buf = last_buf;
+    if (JS_IsString(val)) {
+        goto string;
+    }
 
-    b->memory = (buffer.len ? 1 : 0);
-    b->sync = (buffer.len ? 0 : 1);
-    b->tag = (ngx_buf_tag_t) &ngx_stream_js_module;
+    buf = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL);
+    if (!JS_IsException(buf)) {
+        buffer.data = JS_GetArrayBuffer(cx, &buffer.len, buf);
+        if (buffer.data == NULL) {
+            JS_FreeValue(cx, buf);
+            return JS_EXCEPTION;
+        }
 
-    b->start = buffer.data;
-    b->end = buffer.data + buffer.len;
+        buffer.data += byte_offset;
+        buffer.len = byte_length;
 
-    b->pos = b->start;
-    b->last = b->end;
+    } else {
+string:
 
-    if (from_upstream == NGX_JS_BOOL_UNSET) {
-        *ctx->last_out = cl;
-        ctx->last_out = &cl->next;
+        str = JS_ToCStringLen(cx, &buffer.len, val);
+        if (str == NULL) {
+            return JS_EXCEPTION;
+        }
 
-    } else {
+        buffer.data = (u_char *) str;
+    }
 
-        if (ngx_stream_js_next_filter(s, ctx, cl, from_upstream) == NGX_ERROR) {
-            return JS_ThrowInternalError(cx, "ngx_stream_js_next_filter() "
-                                         "failed");
+    do {
+        cl = ngx_chain_get_free_buf(c->pool, &ctx->free);
+        if (cl == NULL) {
+            goto out_of_memory;
+        }
+
+        b = cl->buf;
+
+        if (b->start == NULL) {
+            b->start = ngx_pnalloc(c->pool, buffer.len);
+            if (b->start == NULL) {
+                goto out_of_memory;
+            }
+
+            len = buffer.len;
+            b->end = b->start + len;
+
+        } else {
+            len = ngx_min(buffer.len, (size_t) (b->end - b->start));
+        }
+
+        memcpy(b->start, buffer.data, len);
+
+        b->pos = b->start;
+        b->last = b->start + len;
+
+        if (buffer.len == len) {
+            b->last_buf = last_buf;
+            b->flush = flush;
+
+        } else {
+            b->last_buf = 0;
+            b->flush = 0;
         }
+
+        b->memory = (len ? 1 : 0);
+        b->sync = (len ? 0 : 1);
+        b->tag = (ngx_buf_tag_t) &ngx_stream_js_module;
+
+        buffer.data += len;
+        buffer.len -= len;
+
+        if (from_upstream == NGX_JS_BOOL_UNSET) {
+            *ctx->last_out = cl;
+            ctx->last_out = &cl->next;
+
+        } else {
+
+            if (ngx_stream_js_next_filter(s, ctx, cl, from_upstream)
+                == NGX_ERROR)
+            {
+                if (str != NULL) {
+                    JS_FreeCString(cx, str);
+                }
+
+                return JS_ThrowInternalError(cx, "ngx_stream_js_next_filter() "
+                                             "failed");
+            }
+        }
+
+    } while (buffer.len != 0);
+
+    if (str != NULL) {
+        JS_FreeCString(cx, str);
     }
 
+    JS_FreeValue(cx, buf);
+
     return JS_UNDEFINED;
+
+out_of_memory:
+
+    if (str != NULL) {
+        JS_FreeCString(cx, str);
+    }
+
+    JS_FreeValue(cx, buf);
+
+    return JS_ThrowInternalError(cx, "memory error");
 }