]> git.kaiwu.me - njs.git/commitdiff
Make Error.stack faster.
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 29 Jan 2026 02:36:39 +0000 (18:36 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 5 Feb 2026 23:32:12 +0000 (15:32 -0800)
Previously, error.stack reported full names for native function (for
example Array.prototype.map -> map), but it was achieved by iteration
through a global object which is slow.

The fix is to report only function name at hand, this loses a bit
of verbosity but make it ~100 times faster.

make benchmark
before
        ...
exception.stack: 38.421µs, 26027 times/s
exception.native.stack: 226.711µs, 4410 times/s
after
        ...
exception.stack: 1.239µs, 807356 times/s
exception.native.stack: 2.419µs, 413339 times/s

src/njs_builtin.c
src/njs_error.c
src/njs_function.c
src/njs_vm.h
src/test/njs_benchmark.c
src/test/njs_unit_test.c
test/shell_test_njs.exp

index 1b486ba90574b208dffe4e3f8884c3c5d2bb168b..ff4352a71c45c0ea406726872d42efc6ead7ecd4 100644 (file)
 #include <signal.h>
 
 
-typedef struct {
-    enum {
-       NJS_BUILTIN_TRAVERSE_KEYS,
-       NJS_BUILTIN_TRAVERSE_MATCH,
-    }                          type;
-
-    njs_function_t             *func;
-
-    njs_flathsh_t              keys;
-    njs_str_t                  match;
-} njs_builtin_traverse_t;
-
-
 typedef struct {
     njs_str_t       name;
     int             value;
@@ -312,312 +299,6 @@ njs_builtin_objects_create(njs_vm_t *vm)
 }
 
 
-static njs_int_t
-njs_builtin_traverse(njs_vm_t *vm, njs_traverse_t *traverse, void *data)
-{
-    size_t                  len;
-    u_char                  *p, *start, *end;
-    njs_int_t               ret, n;
-    njs_str_t               name;
-    njs_bool_t              symbol;
-    njs_value_t             key, *value, prop_name;
-    njs_function_t          *func, *target;
-    njs_object_prop_t       *prop;
-    njs_flathsh_query_t     fhq;
-    njs_builtin_traverse_t  *ctx;
-    njs_traverse_t          *path[NJS_TRAVERSE_MAX_DEPTH];
-    u_char                  buf[256];
-
-    ctx = data;
-
-    if (ctx->type == NJS_BUILTIN_TRAVERSE_MATCH) {
-        prop = traverse->prop;
-        func = ctx->func;
-
-        if (njs_is_accessor_descriptor(prop)) {
-            target = njs_prop_getter(prop);
-
-        } else {
-            value = njs_prop_value(prop);
-            target = (njs_is_function(value) && njs_function(value)->native)
-                            ? njs_function(value)
-                            : NULL;
-        }
-
-        if (target == NULL
-            || !njs_native_function_same(target, func))
-        {
-            return NJS_OK;
-        }
-    }
-
-    if (traverse == NULL) {
-        njs_type_error(vm, "njs_builtin_traverse() traverse arg is NULL");
-        return NJS_ERROR;
-    }
-
-    n = 0;
-
-    while (traverse != NULL) {
-        path[n++] = traverse;
-        traverse = traverse->parent;
-    }
-
-    n--;
-
-    p = buf;
-    end = buf + sizeof(buf);
-
-    do {
-        symbol = 0;
-
-        ret = njs_atom_to_value(vm, &key, path[n]->atom_id);
-        if (ret != NJS_OK) {
-            return NJS_ERROR;
-        }
-
-        if (njs_slow_path(njs_is_symbol(&key))) {
-            symbol = 1;
-            key = *njs_symbol_description(&key);
-            if (njs_is_undefined(&key)) {
-                njs_set_empty_string(vm, &key);
-            }
-        }
-
-        if (njs_slow_path(!njs_is_string(&key))) {
-            /* Skipping special properties (e.g. array index properties). */
-            return NJS_OK;
-        }
-
-        njs_string_get(vm, &key, &name);
-
-        if (njs_slow_path((p + name.length + 3) > end)) {
-            njs_type_error(vm, "njs_builtin_traverse() key is too long");
-            return NJS_ERROR;
-        }
-
-        if (symbol) {
-            *p++ = '[';
-
-        } else if (p != buf) {
-            *p++ = '.';
-        }
-
-        p = njs_cpymem(p, name.start, name.length);
-
-        if (symbol) {
-            *p++ = ']';
-        }
-
-    } while (n-- > 0);
-
-    if (ctx->type == NJS_BUILTIN_TRAVERSE_MATCH) {
-        len = ctx->match.length;
-        start = njs_mp_alloc(vm->mem_pool, len + (p - buf) + (len != 0));
-        if (njs_slow_path(start == NULL)) {
-            njs_memory_error(vm);
-            return NJS_ERROR;
-        }
-
-        if (len != 0) {
-            memcpy(start, ctx->match.start, len);
-            start[len++] = '.';
-        }
-
-        memcpy(start + len, buf, p - buf);
-        ctx->match.length = len + p - buf;
-        ctx->match.start = start;
-
-        return NJS_DONE;
-    }
-
-    /* NJS_BUILTIN_TRAVERSE_KEYS. */
-
-    ret = njs_atom_string_create(vm, &prop_name, buf, p - buf);
-    if (njs_slow_path(ret != NJS_OK)) {
-        return ret;
-    }
-
-    fhq.key_hash = prop_name.atom_id;
-    fhq.replace = 1;
-    fhq.pool = vm->mem_pool;
-    fhq.proto = &njs_object_hash_proto;
-
-    ret = njs_flathsh_unique_insert(&ctx->keys, &fhq);
-    if (njs_slow_path(ret != NJS_OK)) {
-        njs_internal_error(vm, "flathsh insert/replace failed");
-        return NJS_ERROR;
-    }
-
-    prop = fhq.value;
-
-    prop->type = NJS_PROPERTY;
-    prop->enumerable = 0;
-    prop->configurable = 0;
-    prop->writable = 0;
-    prop->u.value = njs_value_null;
-
-    return NJS_OK;
-}
-
-
-typedef struct {
-    njs_str_t               name;
-    njs_function_native_t   native;
-    uint8_t                 magic8;
-} njs_function_name_t;
-
-
-njs_int_t
-njs_builtin_match_native_function(njs_vm_t *vm, njs_function_t *function,
-    njs_str_t *name)
-{
-    uint8_t                 magic8;
-    njs_int_t               ret;
-    njs_arr_t               **pprotos;
-    njs_mod_t               *module;
-    njs_uint_t              i, n;
-    njs_value_t             value, tag;
-    njs_object_t            object;
-    njs_object_prop_t       *prop;
-    njs_flathsh_each_t      lhe;
-    njs_exotic_slots_t      *slots;
-    njs_function_name_t     *fn;
-    njs_function_native_t   native;
-    njs_builtin_traverse_t  ctx;
-
-    if (vm->functions_name_cache != NULL) {
-        n = vm->functions_name_cache->items;
-        fn = vm->functions_name_cache->start;
-
-        magic8 = function->magic8;
-        native = function->u.native;
-
-        while (n != 0) {
-            if (fn->native == native && fn->magic8 == magic8) {
-                *name = fn->name;
-                return NJS_OK;
-            }
-
-            fn++;
-            n--;
-        }
-    }
-
-    ctx.type = NJS_BUILTIN_TRAVERSE_MATCH;
-    ctx.func = function;
-
-    /* Global object. */
-
-    ctx.match = njs_str_value("");
-
-    ret = njs_object_traverse(vm, njs_object(&vm->global_value), &ctx,
-                              njs_builtin_traverse);
-
-    if (ret == NJS_DONE) {
-        goto found;
-    }
-
-    /* Constructor from built-in modules (not-mapped to global object). */
-
-    for (i = NJS_OBJ_TYPE_HIDDEN_MIN; i < NJS_OBJ_TYPE_HIDDEN_MAX; i++) {
-        njs_set_object(&value, &njs_vm_ctor(vm, i).object);
-
-        ret = njs_value_property(vm, &value, NJS_ATOM_STRING_name, &tag);
-        if (ret == NJS_OK && njs_is_string(&tag)) {
-            njs_string_get(vm, &tag, &ctx.match);
-        }
-
-        ret = njs_object_traverse(vm, njs_object(&value), &ctx,
-                                  njs_builtin_traverse);
-
-        if (ret == NJS_DONE) {
-            goto found;
-        }
-    }
-
-    /* Modules. */
-
-    njs_flathsh_each_init(&lhe, &njs_modules_hash_proto);
-
-    for ( ;; ) {
-        prop = (njs_object_prop_t *) njs_flathsh_each(&vm->modules_hash, &lhe);
-        if (prop == NULL) {
-            break;
-        }
-
-        module = prop->u.mod;
-
-        if (njs_is_object(&module->value)
-            && !njs_object(&module->value)->shared)
-        {
-            ctx.match = module->name;
-
-            ret = njs_object_traverse(vm, njs_object(&module->value), &ctx,
-                                      njs_builtin_traverse);
-
-            if (ret == NJS_DONE) {
-                goto found;
-            }
-        }
-    }
-
-    /* External prototypes (not mapped to global object). */
-
-    ctx.match = njs_str_value("");
-
-    for (i = 0; i< vm->protos->items; i++) {
-        njs_memzero(&object, sizeof(njs_object_t));
-
-        pprotos = njs_arr_item(vm->protos, i);
-        slots = (*pprotos)->start;
-
-        object.shared_hash = slots->external_shared_hash;
-        object.slots = slots;
-
-        njs_set_object(&value, &object);
-
-        ret = njs_object_string_tag(vm, &value, &tag);
-        if (ret == NJS_OK && njs_is_string(&tag)) {
-            njs_string_get(vm, &tag, &ctx.match);
-        }
-
-        ret = njs_object_traverse(vm, njs_object(&value), &ctx,
-                                  njs_builtin_traverse);
-
-        if (ret == NJS_DONE) {
-            goto found;
-        }
-    }
-
-    return NJS_DECLINED;
-
-found:
-
-    if (vm->functions_name_cache == NULL) {
-        vm->functions_name_cache = njs_arr_create(vm->mem_pool, 4,
-                                                  sizeof(njs_function_name_t));
-        if (njs_slow_path(vm->functions_name_cache == NULL)) {
-            return NJS_ERROR;
-        }
-    }
-
-    fn = njs_arr_add(vm->functions_name_cache);
-    if (njs_slow_path(fn == NULL)) {
-        njs_memory_error(vm);
-        return NJS_ERROR;
-    }
-
-    fn->name = ctx.match;
-    fn->native = function->u.native;
-    fn->magic8 = function->magic8;
-
-    *name = fn->name;
-
-    return NJS_OK;
-}
-
-
 static njs_int_t
 njs_ext_dump(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused, njs_value_t *retval)
index e8de835fc5aafb0397b37370b7c20e3447185d1d..13ed32aae51869a1808998762cbd52ecf71a0914 100644 (file)
@@ -70,15 +70,17 @@ njs_error_fmt_new(njs_vm_t *vm, njs_value_t *dst, njs_object_type_t type,
 void
 njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
 {
-    size_t              count;
-    uint32_t            line, prev_line;
-    njs_int_t           ret;
-    njs_str_t           name, file, prev_name;
-    njs_chb_t           chain;
-    njs_value_t         *stackval;
-    njs_vm_code_t       *code;
-    njs_function_t      *function;
-    njs_native_frame_t  *frame;
+    size_t               count;
+    uint32_t             line, prev_line;
+    njs_int_t            ret;
+    njs_str_t            name, file, prev_name;
+    njs_chb_t            chain;
+    njs_value_t          *stackval, retval, fobj;
+    njs_vm_code_t        *code;
+    njs_function_t       *function;
+    njs_object_prop_t    *prop;
+    njs_native_frame_t   *frame;
+    njs_flathsh_query_t  fhq;
 
     if (njs_slow_path(!vm->options.backtrace
                       || !njs_is_error(&value))
@@ -128,9 +130,40 @@ njs_error_stack_attach(njs_vm_t *vm, njs_value_t value)
             }
 
         } else {
-            ret = njs_builtin_match_native_function(vm, function, &name);
-            if (ret != NJS_OK) {
-                name = njs_entry_unknown;
+            name.length = 0;
+            fhq.key_hash = NJS_ATOM_STRING_name;
+
+            ret = njs_flathsh_unique_find(&function->object.hash, &fhq);
+            if (ret == NJS_OK) {
+                prop = fhq.value;
+
+                if (njs_is_string(njs_prop_value(prop))) {
+                    njs_string_get(vm, njs_prop_value(prop), &name);
+                }
+            }
+
+            if (name.length == 0) {
+                njs_set_function(&fobj, function);
+
+                ret = njs_value_property(vm, &fobj, NJS_ATOM_STRING_name,
+                                         &retval);
+                if (ret != NJS_OK) {
+                    continue;
+                }
+
+                if (njs_is_string(&retval)) {
+                    njs_string_get(vm, &retval, &name);
+
+                    if (name.length == 0) {
+                        continue;
+                    }
+
+                } else if (njs_is_symbol(&retval)) {
+                    name = njs_str_value("<symbol>");
+
+                } else {
+                    name = njs_entry_unknown;
+                }
             }
         }
 
index 9ed4bb42ca6ce94cc46343d7a569f6ec4078120c..b6a8a467ec628cdaf5b869217ec99019afc5f0f4 100644 (file)
@@ -617,12 +617,16 @@ njs_function_native_call(njs_vm_t *vm, njs_value_t *retval)
 
 #ifdef NJS_DEBUG_OPCODE
     njs_str_t              name;
+    njs_value_t            fname, fobj;
 
     if (vm->options.opcode_debug) {
+        name = njs_str_value("unmapped");
 
-        ret = njs_builtin_match_native_function(vm, function, &name);
-        if (ret != NJS_OK) {
-           name = njs_str_value("unmapped");
+        njs_set_function(&fobj, function);
+
+        ret = njs_value_property(vm, &fobj, NJS_ATOM_STRING_name, &fname);
+        if (ret == NJS_OK && njs_is_string(&fname)) {
+            njs_string_get(vm, &fname, &name);
         }
 
         njs_printf("CALL NATIVE %V %P\n", &name, function->u.native);
index 53c14c9e6169e96309d1d0c74a0d0d86f5dba50a..4329c926997b2ec5e12f05086dfe37a304a4f70a 100644 (file)
@@ -177,7 +177,6 @@ struct njs_vm_s {
     njs_value_t              global_value;
 
     njs_arr_t                *codes;  /* of njs_vm_code_t */
-    njs_arr_t                *functions_name_cache;
 
     njs_trace_t              trace;
     njs_random_t             random;
@@ -247,8 +246,6 @@ njs_value_t njs_vm_exception(njs_vm_t *vm);
 void njs_vm_scopes_restore(njs_vm_t *vm, njs_native_frame_t *frame);
 
 njs_int_t njs_builtin_objects_create(njs_vm_t *vm);
-njs_int_t njs_builtin_match_native_function(njs_vm_t *vm,
-    njs_function_t *function, njs_str_t *name);
 
 void njs_disassemble(u_char *start, u_char *end, njs_int_t count,
     njs_arr_t *lines);
index 7f1a740031d7f889fb320ace725a165b3cf6d43a..e83e239c10a84cb032004b06c37e6d44489d0773 100644 (file)
@@ -477,7 +477,17 @@ static njs_benchmark_test_t  njs_test[] =
 
     { "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"),
+      njs_str("Error: test\n    at f (:1)\n    at anonymous (:1)\n    at map (native)\n    at main (:1)\n"),
+      100 },
+
+    { "exception.native.stack",
+      njs_str("function f() { try { 'str'.replace(/t/g,function(m) {return m.a.a}) } catch (e) { return e.stack } }; f()"),
+      njs_str("TypeError: cannot get property \"a\" of undefined\n"
+              "    at anonymous (:1)\n"
+              "    at [Symbol.replace] (native)\n"
+              "    at replace (native)\n"
+              "    at f (:1)\n"
+              "    at main (:1)\n"),
       100 },
 };
 
index 9de4eb4186515d182008566eac60f86aaedaec59..84a8709ac76fc16e9543889a7f5e9fc3d79a17c0 100644 (file)
@@ -21138,12 +21138,12 @@ static njs_unit_test_t  njs_shared_test[] =
 
     { njs_str("var fs = require('fs'); fs.readFileSync()"),
       njs_str("TypeError: \"path\" must be a string or Buffer\n"
-              "    at fs.readFileSync (native)\n"
+              "    at readFileSync (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("import fs from 'fs'; fs.readFileSync()"),
       njs_str("TypeError: \"path\" must be a string or Buffer\n"
-              "    at fs.readFileSync (native)\n"
+              "    at readFileSync (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("var f = new Function('return 1;'); f();"),
@@ -21193,8 +21193,8 @@ static njs_unit_test_t  njs_shared_test[] =
 
     { njs_str("Array.prototype.push.call(preload.a, 'waka')"),
       njs_str("TypeError: Cannot add property \"2\", object is not extensible\n"
-              "    at Array.prototype.push (native)\n"
-              "    at Function.prototype.call (native)\n"
+              "    at push (native)\n"
+              "    at call (native)\n"
               "    at main (:1)\n") },
 };
 
@@ -21544,42 +21544,42 @@ static njs_unit_test_t  njs_backtraces_test[] =
               "              function(m) {return m.a.a})"),
       njs_str("TypeError: cannot get property \"a\" of undefined\n"
               "    at anonymous (:1)\n"
-              "    at RegExp.prototype[Symbol.replace] (native)\n"
-              "    at String.prototype.replace (native)\n"
+              "    at [Symbol.replace] (native)\n"
+              "    at replace (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("function f(o) {return Object.keys(o)};"
               "f()"),
       njs_str("TypeError: cannot convert undefined argument to object\n"
-              "    at Object.keys (native)\n"
+              "    at keys (native)\n"
               "    at f (:1)\n"
               "    at main (:1)\n") },
 
     { njs_str("[].concat({}.a.a)"),
       njs_str("TypeError: cannot get property \"a\" of undefined\n"
-              "    at Array.prototype.concat (native)\n"
+              "    at concat (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("''.repeat(-1)"),
       njs_str("RangeError: invalid count value\n"
-              "    at String.prototype.repeat (native)\n"
+              "    at repeat (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("Math.log({}.a.a)"),
       njs_str("TypeError: cannot get property \"a\" of undefined\n"
-              "    at Math.log (native)\n"
+              "    at log (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("var bound = Math.max.bind(null, {toString(){return {}}}); bound(1)"),
       njs_str("TypeError: Cannot convert object to primitive value\n"
-              "    at Math.max (native)\n"
+              "    at max (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("var ab = new ArrayBuffer(1);"
               "$262.detachArrayBuffer(ab);"
               "ab.slice(0)"),
       njs_str("TypeError: detached buffer\n"
-              "    at ArrayBuffer.prototype.slice (native)\n"
+              "    at slice (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("Object.prototype()"),
@@ -21593,7 +21593,7 @@ static njs_unit_test_t  njs_backtraces_test[] =
 
     { njs_str("$shared.method({}.a.a)"),
       njs_str("TypeError: cannot get property \"a\" of undefined\n"
-              "    at $shared.method (native)\n"
+              "    at method (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("new Function(\n\n@)"),
@@ -21606,24 +21606,24 @@ static njs_unit_test_t  njs_backtraces_test[] =
 
     { njs_str("require('crypto').createHash('sha')"),
       njs_str("TypeError: not supported algorithm: \"sha\"\n"
-              "    at crypto.createHash (native)\n"
+              "    at createHash (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("var h = require('crypto').createHash('sha1');"
               "h.update([])"),
       njs_str("TypeError: data is not a string or Buffer-like object\n"
-              "    at Hash.update (native)\n"
+              "    at update (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("require('crypto').createHmac('sha1', [])"),
       njs_str("TypeError: key is not a string or Buffer-like object\n"
-              "    at crypto.createHmac (native)\n"
+              "    at createHmac (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("var h = require('crypto').createHmac('sha1', 'secret');"
               "h.update([])"),
       njs_str("TypeError: data is not a string or Buffer-like object\n"
-              "    at Hmac.update (native)\n"
+              "    at update (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("function f(o) {function f_in(o) {return o.a.a};"
@@ -21659,7 +21659,7 @@ static njs_unit_test_t  njs_backtraces_test[] =
               " 'realpath',"
               " 'realpathSync',"
               "]"
-              ".every(v=>{ try {fs[v]();} catch (e) { return e.stack.search(`fs.${v} `) >= 0}})"),
+              ".every(v=>{ try {fs[v]();} catch (e) { return e.stack.search(`at ${v} `) >= 0}})"),
       njs_str("true") },
 
     { njs_str("parseInt({ toString: function() { return [1] } })"),
@@ -21673,7 +21673,7 @@ static njs_unit_test_t  njs_backtraces_test[] =
     { njs_str("Object.defineProperty(Function.__proto__, 'name', {get() { typeof 1;}});"
               "(new Uint8Array()).every()"),
       njs_str("TypeError: callback argument is not callable\n"
-              "    at TypedArray.prototype.every (native)\n"
+              "    at every (native)\n"
               "    at main (:1)\n") },
 
     { njs_str("var e = new Error('oops'); e.stack = 123; e.stack"),
@@ -21722,7 +21722,7 @@ static njs_unit_test_t  njs_backtraces_test[] =
 
     { njs_str("Math\n.min(1,\na)"),
       njs_str("ReferenceError: \"a\" is not defined\n"
-              "    at Math.min (native)\n"
+              "    at min (native)\n"
               "    at main (:3)\n") },
 };
 
index 9c15b81a22091b460671b40801dff037f14f2a16..6dea0442ee8efe054f4a6d1487de35c545aea6b7 100644 (file)
@@ -67,7 +67,7 @@ njs_test {
 # Backtraces for external objects
 njs_test {
     {"console.info(console.a.a)\r\n"
-     "console.info(console.a.a)\r\nThrown:\r\nTypeError:*at console.info (native)"}
+     "console.info(console.a.a)\r\nThrown:\r\nTypeError:*at info (native)"}
 }
 
 # dumper