[njs] Implemented lazy stack symbolization.

noreply at nginx.com noreply at nginx.com
Thu Oct 17 05:42:02 UTC 2024


details:   https://github.com/nginx/njs/commit/eda6fa7d430619f646a2c969a267225306cff521
branches:  master
commit:    eda6fa7d430619f646a2c969a267225306cff521
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Tue, 15 Oct 2024 18:28:19 -0700
description:
Implemented lazy stack symbolization.

Previously, when an exception was thrown, the exception got 'stack'
property attached which contained the backtrace information about where
the exception happened. This could be a heavy operation and it was not
always needed.

To optimize it, the process is split into 2 phases. The first phase
collects all the necessary info about the current stack. The second
phase, where the stack symbolization happens, occurs only when this
property is referenced.

---
 src/njs_error.c          | 182 +++++++++++++++++++++++++++++++++++------------
 src/njs_vm.h             |   2 +-
 src/test/njs_benchmark.c |  11 +++
 3 files changed, 150 insertions(+), 45 deletions(-)

diff --git a/src/njs_error.c b/src/njs_error.c
index 376205d7..6a14d767 100644
--- a/src/njs_error.c
+++ b/src/njs_error.c
@@ -8,6 +8,15 @@
 #include <njs_main.h>
 
 
+typedef struct {
+    union {
+        njs_function_t                *function;
+        u_char                        *pc;
+    } u;
+    uint8_t                           native;
+} njs_stack_entry_t;
+
+
 typedef struct {
     njs_str_t                         name;
     njs_str_t                         file;
@@ -16,7 +25,7 @@ typedef struct {
 
 
 static njs_int_t njs_add_backtrace_entry(njs_vm_t *vm, njs_arr_t *stack,
-    njs_native_frame_t *native_frame);
+    njs_stack_entry_t *se);
 static njs_int_t njs_backtrace_to_string(njs_vm_t *vm, njs_arr_t *backtrace,
     njs_str_t *dst);
 
@@ -87,22 +96,13 @@ njs_error_fmt_new(njs_vm_t *vm, njs_value_t *dst, njs_object_type_t type,
 
 
 static njs_int_t
-njs_error_stack_new(njs_vm_t *vm, njs_object_t *error, njs_value_t *retval)
+njs_error_stack_new(njs_vm_t *vm, njs_object_value_t *error)
 {
-    njs_int_t           ret;
-    njs_str_t           string;
     njs_arr_t           *stack;
-    njs_value_t         value;
+    njs_stack_entry_t   *se;
     njs_native_frame_t  *frame;
 
-    njs_set_object(&value, error);
-
-    ret = njs_error_to_string(vm, retval, &value);
-    if (njs_slow_path(ret != NJS_OK)) {
-        return ret;
-    }
-
-    stack = njs_arr_create(vm->mem_pool, 4, sizeof(njs_backtrace_entry_t));
+    stack = njs_arr_create(vm->mem_pool, 4, sizeof(njs_stack_entry_t));
     if (njs_slow_path(stack == NULL)) {
         return NJS_ERROR;
     }
@@ -110,10 +110,20 @@ njs_error_stack_new(njs_vm_t *vm, njs_object_t *error, njs_value_t *retval)
     frame = vm->top_frame;
 
     for ( ;; ) {
-        if ((frame->native || frame->pc != NULL)
-            && njs_add_backtrace_entry(vm, stack, frame) != NJS_OK)
-        {
-            break;
+        if (frame->native || frame->pc != NULL) {
+            se = njs_arr_add(stack);
+            if (njs_slow_path(se == NULL)) {
+                return NJS_ERROR;
+            }
+
+            se->native = frame->native;
+
+            if (se->native) {
+                se->u.function = frame->function;
+
+            } else {
+                se->u.pc = frame->pc;
+            }
         }
 
         frame = frame->previous;
@@ -123,25 +133,16 @@ njs_error_stack_new(njs_vm_t *vm, njs_object_t *error, njs_value_t *retval)
         }
     }
 
-    njs_string_get(retval, &string);
-
-    ret = njs_backtrace_to_string(vm, stack, &string);
-
-    njs_arr_destroy(stack);
-
-    if (njs_slow_path(ret != NJS_OK)) {
-        return ret;
-    }
+    njs_data(&error->value) = stack;
 
-    return njs_string_create(vm, retval, string.start, string.length);
+    return NJS_OK;
 }
 
 
 njs_int_t
 njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
 {
-    njs_int_t    ret;
-    njs_value_t  stack;
+    njs_int_t  ret;
 
     if (njs_slow_path(!njs_is_error(&value))
         || njs_object(&value)->stack_attached)
@@ -153,7 +154,7 @@ njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
         return NJS_OK;
     }
 
-    ret = njs_error_stack_new(vm, njs_object(&value), &stack);
+    ret = njs_error_stack_new(vm, value.data.u.object_value);
     if (njs_slow_path(ret != NJS_OK)) {
         njs_internal_error(vm, "njs_error_stack_new() failed");
         return NJS_ERROR;
@@ -161,10 +162,7 @@ njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
 
     njs_object(&value)->stack_attached = 1;
 
-    return njs_object_prop_define(vm, &value,
-                                  njs_value_arg(&njs_error_stack_string),
-                                  &stack, NJS_OBJECT_PROP_VALUE_CW,
-                                  NJS_STACK_HASH);
+    return NJS_OK;
 }
 
 
@@ -194,16 +192,20 @@ njs_error_alloc(njs_vm_t *vm, njs_object_t *proto, const njs_value_t *name,
     njs_int_t           ret;
     njs_object_t        *error;
     njs_object_prop_t   *prop;
+    njs_object_value_t  *ov;
     njs_lvlhsh_query_t  lhq;
 
-    error = njs_mp_alloc(vm->mem_pool, sizeof(njs_object_t));
-    if (njs_slow_path(error == NULL)) {
+    ov = njs_mp_alloc(vm->mem_pool, sizeof(njs_object_value_t));
+    if (njs_slow_path(ov == NULL)) {
         goto memory_error;
     }
 
+    njs_set_data(&ov->value, NULL, NJS_DATA_TAG_ANY);
+
+    error = &ov->object;
     njs_lvlhsh_init(&error->hash);
     njs_lvlhsh_init(&error->shared_hash);
-    error->type = NJS_OBJECT;
+    error->type = NJS_OBJECT_VALUE;
     error->shared = 0;
     error->extensible = 1;
     error->fast_array = 0;
@@ -485,15 +487,18 @@ const njs_object_init_t  njs_aggregate_error_constructor_init = {
 void
 njs_memory_error_set(njs_vm_t *vm, njs_value_t *value)
 {
-    njs_object_t  *object;
+    njs_object_t        *object;
+    njs_object_value_t  *ov;
 
-    object = &vm->memory_error_object;
+    ov = &vm->memory_error_object;
+    njs_set_data(&ov->value, NULL, NJS_DATA_TAG_ANY);
 
+    object = &ov->object;
     njs_lvlhsh_init(&object->hash);
     njs_lvlhsh_init(&object->shared_hash);
     object->__proto__ = njs_vm_proto(vm, NJS_OBJ_TYPE_INTERNAL_ERROR);
     object->slots = NULL;
-    object->type = NJS_OBJECT;
+    object->type = NJS_OBJECT_VALUE;
     object->shared = 1;
 
     /*
@@ -691,6 +696,92 @@ njs_error_prototype_to_string(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 }
 
 
+static njs_int_t
+njs_error_prototype_stack(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    njs_int_t          ret;
+    njs_str_t          string;
+    njs_arr_t          *stack, *backtrace;
+    njs_uint_t         i;
+    njs_value_t        rv, *stackval;
+    njs_stack_entry_t  *se;
+
+    if (retval != NULL) {
+        if (!njs_is_error(value)) {
+            njs_set_undefined(retval);
+            return NJS_DECLINED;
+        }
+
+        stackval = njs_object_value(value);
+
+        if (setval != NULL) {
+            njs_value_assign(stackval, setval);
+            return NJS_OK;
+        }
+
+        if (!njs_is_data(stackval, NJS_DATA_TAG_ANY)) {
+            njs_value_assign(retval, stackval);
+            return NJS_OK;
+        }
+
+        stack = njs_data(stackval);
+        if (stack == NULL) {
+            njs_set_undefined(retval);
+            return NJS_OK;
+        }
+
+        se = stack->start;
+
+        backtrace = njs_arr_create(vm->mem_pool, stack->items,
+                                   sizeof(njs_backtrace_entry_t));
+        if (njs_slow_path(backtrace == NULL)) {
+            return NJS_ERROR;
+        }
+
+        for (i = 0; i < stack->items; i++) {
+            if (njs_add_backtrace_entry(vm, backtrace, &se[i]) != NJS_OK) {
+                return NJS_ERROR;
+            }
+        }
+
+        ret = njs_error_to_string2(vm, &rv, value, 0);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
+
+        njs_string_get(&rv, &string);
+
+        ret = njs_backtrace_to_string(vm, backtrace, &string);
+
+        njs_arr_destroy(backtrace);
+        njs_arr_destroy(stack);
+
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
+
+        ret = njs_string_create(vm, stackval, string.start, string.length);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
+
+        njs_value_assign(retval, stackval);
+
+        return NJS_OK;
+    }
+
+    /* Delete. */
+
+    if (njs_is_error(value)) {
+        stackval = njs_object_value(value);
+        njs_set_data(stackval, NULL, NJS_DATA_TAG_ANY);
+    }
+
+    return NJS_OK;
+}
+
+
 njs_int_t
 njs_error_to_string(njs_vm_t *vm, njs_value_t *retval, const njs_value_t *error)
 {
@@ -717,6 +808,9 @@ static const njs_object_prop_t  njs_error_prototype_properties[] =
     NJS_DECLARE_PROP_NATIVE("valueOf", njs_error_prototype_value_of, 0, 0),
 
     NJS_DECLARE_PROP_NATIVE("toString", njs_error_prototype_to_string, 0, 0),
+
+    NJS_DECLARE_PROP_HANDLER("stack", njs_error_prototype_stack,
+                             0, 0, NJS_OBJECT_PROP_VALUE_CW),
 };
 
 
@@ -986,14 +1080,14 @@ const njs_object_type_init_t  njs_aggregate_error_type_init = {
 
 static njs_int_t
 njs_add_backtrace_entry(njs_vm_t *vm, njs_arr_t *stack,
-    njs_native_frame_t *native_frame)
+    njs_stack_entry_t *se)
 {
     njs_int_t              ret;
     njs_vm_code_t          *code;
     njs_function_t         *function;
     njs_backtrace_entry_t  *be;
 
-    function = native_frame->function;
+    function = se->native ? se->u.function : NULL;
 
     if (function != NULL && function->bound != NULL) {
         /* Skip. */
@@ -1019,7 +1113,7 @@ njs_add_backtrace_entry(njs_vm_t *vm, njs_arr_t *stack,
         return NJS_OK;
     }
 
-    code = njs_lookup_code(vm, native_frame->pc);
+    code = njs_lookup_code(vm, se->u.pc);
 
     if (code != NULL) {
         be->name = code->name;
@@ -1028,7 +1122,7 @@ njs_add_backtrace_entry(njs_vm_t *vm, njs_arr_t *stack,
             be->name = njs_entry_anonymous;
         }
 
-        be->line = njs_lookup_line(code->lines, native_frame->pc - code->start);
+        be->line = njs_lookup_line(code->lines, se->u.pc - code->start);
         if (!vm->options.quiet) {
             be->file = code->file;
         }
diff --git a/src/njs_vm.h b/src/njs_vm.h
index 6d09caf8..a3f2b5a9 100644
--- a/src/njs_vm.h
+++ b/src/njs_vm.h
@@ -165,7 +165,7 @@ struct njs_vm_s {
      * MemoryError is statically allocated immutable Error object
      * with the InternalError prototype.
      */
-    njs_object_t             memory_error_object;
+    njs_object_value_t       memory_error_object;
 
     njs_object_t             string_object;
     njs_object_t             global_object;
diff --git a/src/test/njs_benchmark.c b/src/test/njs_benchmark.c
index 6ca83e82..98c618c8 100644
--- a/src/test/njs_benchmark.c
+++ b/src/test/njs_benchmark.c
@@ -116,6 +116,7 @@ njs_benchmark_test(njs_vm_t *parent, njs_opts_t *opts, njs_value_t *report,
 
     njs_vm_opt_init(&options);
 
+    options.backtrace = 1;
     options.addons = njs_benchmark_addon_external_modules;
 
     vm = NULL;
@@ -468,6 +469,16 @@ static njs_benchmark_test_t  njs_test[] =
       njs_str("$shared.method('YES')"),
       njs_str("shared"),
       1000 },
+
+    { "exception",
+      njs_str("function f() { try { throw new Error('test') } catch (e) { return e.message } } [f].map(v=>v())[0]"),
+      njs_str("test"),
+      10000 },
+
+    { "exception.stack",
+      njs_str("function f() { try { throw new Error('test') } catch (e) { return e.stack } } [f].map(v=>v())[0]"),
+      njs_str("Error: test\n    at f (:1)\n    at anonymous (:1)\n    at Array.prototype.map (native)\n    at main (:1)\n"),
+      100 },
 };
 
 


More information about the nginx-devel mailing list