fix(3517): super-simplistic fix to avoid costly AST transforms when t… (#4292)

* fix(3517): super-simplistic fix to avoid costly AST transforms when they are not needed

Co-authored-by: Ronny Berndt <ronny@apache.org>
diff --git a/.gitignore b/.gitignore
index 816ece6..7c2b9f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,7 @@
 rel/tmpdata
 share/server/main-coffee.js
 share/server/main.js
+share/server/main-ast-bypass.js
 share/www
 src/bear/
 src/certifi/
diff --git a/Makefile b/Makefile
index 34562d1..522acaf 100644
--- a/Makefile
+++ b/Makefile
@@ -442,7 +442,7 @@
 	@rm -rf src/*/.rebar
 	@rm -rf src/*/priv/*.so
 	@rm -rf src/couch/priv/{couchspawnkillable,couchjs}
-	@rm -rf share/server/main.js share/server/main-coffee.js
+	@rm -rf share/server/main.js share/server/main-ast-bypass.js share/server/main-coffee.js
 	@rm -rf tmp dev/data dev/lib dev/logs
 	@rm -rf src/mango/.venv
 	@rm -f src/couch/priv/couchspawnkillable
diff --git a/Makefile.win b/Makefile.win
index 5bbfeea..8a8129f 100644
--- a/Makefile.win
+++ b/Makefile.win
@@ -374,7 +374,7 @@
 	-@rmdir /s/q src\*\.rebar
 	-@del /f/q/s src\*.dll
 	-@del /f/q src\couch\priv\*.exe
-	-@del /f/q share\server\main.js share\server\main-coffee.js
+	-@del /f/q share\server\main.js share\server\main-ast-bypass.js share\server\main-coffee.js
 	-@rmdir /s/q tmp
 	-@rmdir /s/q dev\data
 	-@rmdir /s/q dev\lib
diff --git a/rel/reltool.config b/rel/reltool.config
index ab26fb2..f7c9df1 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -141,6 +141,7 @@
     {copy, "overlay/etc"},
     {copy, "../src/couch/priv/couchjs", "bin/couchjs"},
     {copy, "../share/server/main.js", "share/server/main.js"},
+    {copy, "../share/server/main-ast-bypass.js", "share/server/main-ast-bypass.js"},
     {copy, "../share/server/main-coffee.js", "share/server/main-coffee.js"},
     {copy, "../src/weatherreport/weatherreport", "bin/weatherreport"},
     {copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"},
diff --git a/share/server/60/rewrite_fun.js b/share/server/60/rewrite_fun.js
index 1b27a9d..436fd3d 100644
--- a/share/server/60/rewrite_fun.js
+++ b/share/server/60/rewrite_fun.js
@@ -28,7 +28,7 @@
 
     // If we have a function declaration without an Id, wrap it
     // in an ExpressionStatement and change it into
-    // a FuntionExpression
+    // a FunctionExpression
     if (decl.type == "FunctionDeclaration" && decl.id == null) {
         decl.type = "FunctionExpression";
         ast.body[idx] = {
@@ -41,7 +41,6 @@
     return escodegen.generate(ast);
 }
 
-
 function rewriteFun(funJSON) {
     const fun = JSON.parse(funJSON);
     return JSON.stringify(rewriteFunInt(fun));
@@ -53,4 +52,4 @@
         return rewriteFunInt(fun);
     });
     return JSON.stringify(results);
-}
\ No newline at end of file
+}
diff --git a/share/server/60/rewrite_fun_ast_bypass.js b/share/server/60/rewrite_fun_ast_bypass.js
new file mode 100644
index 0000000..d2c43ca
--- /dev/null
+++ b/share/server/60/rewrite_fun_ast_bypass.js
@@ -0,0 +1,61 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+// Based on the normalizeFunction which can be
+// found here:
+//
+//  https://github.com/dmunch/couch-chakra/blob/master/js/normalizeFunction.js
+
+function rewriteFunInt(fun) {
+    // Skip lengthy AST transforms if the function passed can be
+    // safely wrapped in parentheses.
+    if (fun.startsWith('function') && fun.endsWith('}')) {
+        return '(' + fun + ')'
+    }
+
+    const ast = esprima.parse(fun);
+    let idx = ast.body.length - 1;
+    let decl = {};
+
+    // Search for the first FunctionDeclaration beginning from the end
+    do {
+        decl = ast.body[idx--];
+    } while (idx >= 0 && decl.type !== "FunctionDeclaration");
+    idx++;
+
+    // If we have a function declaration without an Id, wrap it
+    // in an ExpressionStatement and change it into
+    // a FunctionExpression
+    if (decl.type == "FunctionDeclaration" && decl.id == null) {
+        decl.type = "FunctionExpression";
+        ast.body[idx] = {
+            type: "ExpressionStatement",
+            expression: decl
+        };
+    }
+
+    // Generate source from the rewritten AST
+    return escodegen.generate(ast);
+}
+
+function rewriteFun(funJSON) {
+    const fun = JSON.parse(funJSON);
+    return JSON.stringify(rewriteFunInt(fun));
+}
+
+function rewriteFuns(funsJSON) {
+    let funs = JSON.parse(funsJSON);
+    const results = Array.from(funs, (fun) => {
+        return rewriteFunInt(fun);
+    });
+    return JSON.stringify(results);
+}
diff --git a/src/docs/src/config/query-servers.rst b/src/docs/src/config/query-servers.rst
index 1d81665..3bd99c4 100644
--- a/src/docs/src/config/query-servers.rst
+++ b/src/docs/src/config/query-servers.rst
@@ -65,6 +65,19 @@
 
 For more info about the available options, please consult ``couchjs -h``.
 
+.. note::
+    CouchDB versions 3.0.0 to 3.2.2 included a performance regression for
+    custom reduce functions. CouchDB 3.3.0 and later come with an experimental
+    fix to this issue that is included in a separate ``.js`` file.
+
+    To enable the fix, you need to define a custom ``COUCHDB_QUERY_SERVER_JAVASCRIPT``
+    environment variable as outlined above. The path to ``couchjs`` needs to
+    remain the same as you find it on your ``couchdb`` file, and the path to
+    ``main.js`` needs to be set to ``/path/to/couchdb/share/server/main-ast-bypass.js``.
+
+    With a default installation on Linux systems, this is going to be
+    ``COUCHDB_QUERY_SERVER_JAVASCRIPT="/opt/couchdb/bin/couchjs /opt/couchdb/share/server/main-ast-bypass.js"``
+
 .. _Mozilla SpiderMonkey: https://spidermonkey.dev/
 
 .. seealso::
diff --git a/support/build_js.escript b/support/build_js.escript
index 5f1e920..5ff45fa 100644
--- a/support/build_js.escript
+++ b/support/build_js.escript
@@ -68,11 +68,13 @@
         _ ->
             [
                 "share/server/60/esprima.js",
-                "share/server/60/escodegen.js",
-                "share/server/60/rewrite_fun.js"
+                "share/server/60/escodegen.js"
             ]
     end,
 
+    RewriteFunFile = ["share/server/60/rewrite_fun.js"],
+    RewriteFunWithASTBypassFile = ["share/server/60/rewrite_fun_ast_bypass.js"],
+
     Pre = "(function () {\n",
     Post = "})();\n",
 
@@ -85,6 +87,13 @@
             file:write_file(To, FinalBin)
     end,
 
-    ok = Concat(ExtraFiles ++ JsFiles, "share/server/main.js"),
+    case SMVsn of
+        "1.8.5" ->
+            ok = Concat(ExtraFiles ++ JsFiles, "share/server/main.js"),
+            ok = Concat(ExtraFiles ++ JsFiles, "share/server/main-ast-bypass.js");
+        _ ->
+            ok = Concat(RewriteFunFile ++ ExtraFiles ++ JsFiles, "share/server/main.js"),
+            ok = Concat(RewriteFunWithASTBypassFile ++ ExtraFiles ++ JsFiles, "share/server/main-ast-bypass.js")
+    end,
     ok = Concat(ExtraFiles ++ CoffeeFiles, "share/server/main-coffee.js"),
     ok.