changeset 2607:362ef716eabc

Modules: optimized memory consumption while streaming in qjs. 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.
author Dmitry Volyntsev <xeioex@nginx.com>
date Mon, 18 Aug 2025 21:21:09 -0700
parents caf716d4e693
children 76ccd4e7d5d7
files nginx/ngx_http_js_module.c nginx/ngx_stream_js_module.c
diffstat 2 files changed, 211 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/nginx/ngx_http_js_module.c	Wed Aug 13 23:19:46 2025 -0700
+++ b/nginx/ngx_http_js_module.c	Mon Aug 18 21:21:09 2025 -0700
@@ -5601,10 +5601,12 @@
 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 @@
         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 @@
         JS_FreeValue(cx, value);
     }
 
-    cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
-    if (cl == NULL) {
-        return JS_ThrowOutOfMemory(cx);
-    }
-
-    b = cl->buf;
-
-    b->flush = flush;
-    b->last_buf = last_buf;
-
-    b->memory = (buffer.len ? 1 : 0);
-    b->sync = (buffer.len ? 0 : 1);
-    b->tag = (ngx_buf_tag_t) &ngx_http_js_module;
-
-    b->start = buffer.data;
-    b->end = buffer.data + buffer.len;
-    b->pos = b->start;
-    b->last = b->end;
-
-    *ctx->last_out = cl;
-    ctx->last_out = &cl->next;
+    val = argv[0];
+
+    if (JS_IsNullOrUndefined(val)) {
+        buffer.len = 0;
+        buffer.data = NULL;
+    }
+
+    str = NULL;
+    buf = JS_UNDEFINED;
+
+    if (JS_IsString(val)) {
+        goto string;
+    }
+
+    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;
+        }
+
+        buffer.data += byte_offset;
+        buffer.len = byte_length;
+
+    } 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);
 }
 
 
--- a/nginx/ngx_stream_js_module.c	Wed Aug 13 23:19:46 2025 -0700
+++ b/nginx/ngx_stream_js_module.c	Mon Aug 18 21:21:09 2025 -0700
@@ -2273,10 +2273,12 @@
 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 @@
         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 @@
         }
     }
 
-    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;
-
-    b->flush = flush;
-    b->last_buf = last_buf;
-
-    b->memory = (buffer.len ? 1 : 0);
-    b->sync = (buffer.len ? 0 : 1);
-    b->tag = (ngx_buf_tag_t) &ngx_stream_js_module;
-
-    b->start = buffer.data;
-    b->end = buffer.data + buffer.len;
-
-    b->pos = b->start;
-    b->last = b->end;
-
-    if (from_upstream == NGX_JS_BOOL_UNSET) {
-        *ctx->last_out = cl;
-        ctx->last_out = &cl->next;
+    str = NULL;
+    buf = JS_UNDEFINED;
+
+    if (JS_IsString(val)) {
+        goto string;
+    }
+
+    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;
+        }
+
+        buffer.data += byte_offset;
+        buffer.len = byte_length;
 
     } else {
-
-        if (ngx_stream_js_next_filter(s, ctx, cl, from_upstream) == NGX_ERROR) {
-            return JS_ThrowInternalError(cx, "ngx_stream_js_next_filter() "
-                                         "failed");
+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(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");
 }