This is an automated email from the ASF dual-hosted git repository.
vatamane pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git
The following commit(s) were added to refs/heads/main by this push:
new 0a58f4e24 Add extra timing stats for couch_js engine commands
0a58f4e24 is described below
commit 0a58f4e24a18d8b906be02f3192c4b847ad70787
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Fri Sep 29 15:54:23 2023 -0400
Add extra timing stats for couch_js engine commands
When evaluating various JS engine versions it's useful to track not just
error
metrics, but also how fast commands execute. To avoid creating a large
number
of histograms, and keep the metrics count reasonable, keep track of the
total
accumulated time spent on these commands from the point of view of CouchDB's
Erlang side.
To reduce counting json serialization into the JS processing time, move the
writejson/2 and writeline/2 logic into the `prompt` callback. The time
delta,
is then taken after the data is serialized, and after it's deserialized from
readjson/1. The reason for the asymmetry is that response data could be
streamed back byte-by-byte, so in that case to keep it simpler, opt to
record
the times after json had already been parsed.
Take advantage of updating couch_js_module and switch it to use the more
common
?TDEF macro. Also, take the opportinity to expand the coverage of the
couch_js_module so it also tests reduces, add_lib and some ddoc operations
like
vdu, filter, and updates.
---
src/couch/priv/stats_descriptions.cfg | 40 +++++++
src/couch/src/couch_os_process.erl | 58 +++++++---
src/couch/test/eunit/couch_js_tests.erl | 195 ++++++++++++++++++++++++--------
3 files changed, 230 insertions(+), 63 deletions(-)
diff --git a/src/couch/priv/stats_descriptions.cfg
b/src/couch/priv/stats_descriptions.cfg
index 1983eed9b..578b4c65b 100644
--- a/src/couch/priv/stats_descriptions.cfg
+++ b/src/couch/priv/stats_descriptions.cfg
@@ -330,6 +330,46 @@
{type, counter},
{desc, <<"number of OS process prompt errors">>}
]}.
+{[couchdb, query_server, time, spawn_proc], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent spawning os query
processes">>}
+]}.
+{[couchdb, query_server, time, map], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing map_doc
requests">>}
+]}.
+{[couchdb, query_server, time, reduce], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing reduce
requests">>}
+]}.
+{[couchdb, query_server, time, reset], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing reset
requests">>}
+]}.
+{[couchdb, query_server, time, add_fun], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing add_fun
requests">>}
+]}.
+{[couchdb, query_server, time, ddoc_new], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing ddoc new
requests">>}
+]}.
+{[couchdb, query_server, time, ddoc_vdu], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing vdu
requests">>}
+]}.
+{[couchdb, query_server, time, ddoc_filter], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing filter
requests">>}
+]}.
+{[couchdb, query_server, time, ddoc_other], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing other ddoc
requests">>}
+]}.
+{[couchdb, query_server, time, other], [
+ {type, counter},
+ {desc, <<"accumulated number of microseconds spent processing other
requests">>}
+]}.
{[couchdb, legacy_checksums], [
{type, counter},
{desc, <<"number of legacy checksums found in couch_file instances">>}
diff --git a/src/couch/src/couch_os_process.erl
b/src/couch/src/couch_os_process.erl
index d133a2d96..0302cb01d 100644
--- a/src/couch/src/couch_os_process.erl
+++ b/src/couch/src/couch_os_process.erl
@@ -47,12 +47,8 @@ prompt(Pid, Data) ->
throw(Error)
end.
-% Utility functions for reading and writing lines
+% Utility functions for reading lines and parsing json
%
-writeline(OsProc, Data) when is_record(OsProc, os_proc) ->
- Res = port_command(OsProc#os_proc.port, [Data, $\n]),
- couch_io_logger:log_output(Data),
- Res.
readline(#os_proc{} = OsProc) ->
Res = readline(OsProc, []),
@@ -80,15 +76,6 @@ readline(#os_proc{port = Port} = OsProc, Acc) ->
throw({os_process_error, "OS process timed out."})
end.
-% Standard JSON functions
-writejson(OsProc, Data) when is_record(OsProc, os_proc) ->
- JsonData = ?JSON_ENCODE(Data),
- couch_log:debug(
- "OS Process ~p Input :: ~s",
- [OsProc#os_proc.port, JsonData]
- ),
- true = writeline(OsProc, JsonData).
-
readjson(OsProc) when is_record(OsProc, os_proc) ->
Line = iolist_to_binary(readline(OsProc)),
couch_log:debug("OS Process ~p Output :: ~s", [OsProc#os_proc.port, Line]),
@@ -147,12 +134,16 @@ init([Command]) ->
Spawnkiller = "\"" ++ filename:join(PrivDir, "couchspawnkillable") ++ "\"",
V = config:get("query_server_config", "os_process_idle_limit", "300"),
IdleLimit = list_to_integer(V) * 1000,
+ T0 = erlang:monotonic_time(),
OsProc = #os_proc{
command = Command,
port = open_port({spawn, Spawnkiller ++ " " ++ Command},
?PORT_OPTIONS),
idle = IdleLimit
},
KillCmd = iolist_to_binary(readline(OsProc)),
+ T1 = erlang:monotonic_time(),
+ DtUSec = erlang:convert_time_unit(T1 - T0, native, microsecond),
+ bump_time_stat(spawn_proc, DtUSec),
Pid = self(),
couch_log:debug("OS Process Start :: ~p", [OsProc#os_proc.port]),
couch_stats:increment_counter([couchdb, query_server, process_starts]),
@@ -177,9 +168,17 @@ handle_call({set_timeout, TimeOut}, _From, #os_proc{idle =
Idle} = OsProc) ->
{reply, ok, OsProc#os_proc{timeout = TimeOut}, Idle};
handle_call({prompt, Data}, _From, #os_proc{idle = Idle} = OsProc) ->
try
- writejson(OsProc, Data),
couch_stats:increment_counter([couchdb, query_server,
process_prompts]),
- {reply, {ok, readjson(OsProc)}, OsProc, Idle}
+ JsonData = ?JSON_ENCODE(Data),
+ couch_log:debug("OS Process ~p Input :: ~s", [OsProc#os_proc.port,
JsonData]),
+ couch_io_logger:log_output(JsonData),
+ T0 = erlang:monotonic_time(),
+ true = port_command(OsProc#os_proc.port, [JsonData, $\n]),
+ Response = readjson(OsProc),
+ T1 = erlang:monotonic_time(),
+ DtUSec = erlang:convert_time_unit(T1 - T0, native, microsecond),
+ bump_cmd_time_stat(Data, DtUSec),
+ {reply, {ok, Response}, OsProc, Idle}
catch
throw:{error, OsError} ->
couch_stats:increment_counter([couchdb, query_server,
process_errors]),
@@ -229,3 +228,30 @@ killer(KillCmd) ->
after 1000 ->
killer(KillCmd)
end.
+
+bump_cmd_time_stat(Cmd, USec) when is_list(Cmd), is_integer(USec) ->
+ case Cmd of
+ [<<"map_doc">> | _] ->
+ bump_time_stat(map, USec);
+ [<<"reduce">> | _] ->
+ bump_time_stat(reduce, USec);
+ [<<"rereduce">> | _] ->
+ bump_time_stat(reduce, USec);
+ [<<"reset">> | _] ->
+ bump_time_stat(reset, USec);
+ [<<"add_fun">> | _] ->
+ bump_time_stat(add_fun, USec);
+ [<<"ddoc">>, <<"new">> | _] ->
+ bump_time_stat(ddoc_new, USec);
+ [<<"ddoc">>, _, [<<"validate_doc_update">> | _] | _] ->
+ bump_time_stat(ddoc_vdu, USec);
+ [<<"ddoc">>, _, [<<"filters">> | _] | _] ->
+ bump_time_stat(ddoc_filter, USec);
+ [<<"ddoc">> | _] ->
+ bump_time_stat(ddoc_other, USec);
+ _ ->
+ bump_time_stat(other, USec)
+ end.
+
+bump_time_stat(Stat, USec) when is_atom(Stat), is_integer(USec) ->
+ couch_stats:increment_counter([couchdb, query_server, time, Stat], USec).
diff --git a/src/couch/test/eunit/couch_js_tests.erl
b/src/couch/test/eunit/couch_js_tests.erl
index 6fa6cad8d..94a02b1ea 100644
--- a/src/couch/test/eunit/couch_js_tests.erl
+++ b/src/couch/test/eunit/couch_js_tests.erl
@@ -11,7 +11,7 @@
% the License.
-module(couch_js_tests).
--include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
couch_js_test_() ->
{
@@ -20,22 +20,23 @@ couch_js_test_() ->
setup,
fun test_util:start_couch/0,
fun test_util:stop_couch/1,
- [
- fun should_create_sandbox/0,
- fun should_reset_properly/0,
- fun should_freeze_doc_object/0,
- fun should_roundtrip_utf8/0,
- fun should_roundtrip_modified_utf8/0,
- fun should_replace_broken_utf16/0,
- fun should_allow_js_string_mutations/0,
- {timeout, 60000, fun should_exit_on_oom/0},
- {timeout, 60000, fun should_exit_on_internal_error/0}
- ]
+ with([
+ ?TDEF(should_create_sandbox),
+ ?TDEF(should_reset_properly),
+ ?TDEF(should_freeze_doc_object),
+ ?TDEF(should_roundtrip_utf8),
+ ?TDEF(should_roundtrip_modified_utf8),
+ ?TDEF(should_replace_broken_utf16),
+ ?TDEF(should_allow_js_string_mutations),
+ ?TDEF(should_bump_timing_stats),
+ ?TDEF(should_exit_on_oom, 60000),
+ ?TDEF(should_exit_on_internal_error, 60000)
+ ])
}
}.
%% erlfmt-ignore
-should_create_sandbox() ->
+should_create_sandbox(_) ->
% Try and detect whether we can see out of the
% sandbox or not.
Src = <<"
@@ -48,8 +49,8 @@ should_create_sandbox() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
- Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, {[]}]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
+ Result = prompt(Proc, [<<"map_doc">>, {[]}]),
?assertMatch([[[true, <<_/binary>>]]], Result),
[[[true, ErrMsg]]] = Result,
?assertNotEqual([], binary:matches(ErrMsg, <<"not defined">>)),
@@ -61,7 +62,7 @@ should_create_sandbox() ->
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_reset_properly() ->
+should_reset_properly(_) ->
Src = <<"
function(doc) {
var a = [0,1,2];
@@ -71,20 +72,20 @@ should_reset_properly() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
Doc = {[]},
- Result1 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result1 = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[0, null]]], Result1),
- Result2 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result2 = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[42, 43]]], Result2),
- true = couch_query_servers:proc_prompt(Proc, [<<"reset">>]),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
- Result3 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ true = prompt(Proc, [<<"reset">>]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
+ Result3 = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[0, null]]], Result3),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_freeze_doc_object() ->
+should_freeze_doc_object(_) ->
Src = <<"
function(doc) {
emit(doc.foo, doc.bar);
@@ -94,16 +95,16 @@ should_freeze_doc_object() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
Doc = {[{<<"bar">>, 1041}]},
- Result1 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result1 = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[null, 1041], [null, 1041]]], Result1),
- Result2 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result2 = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[null, 1041], [null, 1041]]], Result2),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_roundtrip_utf8() ->
+should_roundtrip_utf8(_) ->
% Try round tripping UTF-8 both directions through
% couchjs. These tests use hex encoded values of
% Ä (C384) and Ü (C39C) so as to avoid odd editor/Erlang encoding
@@ -114,17 +115,17 @@ should_roundtrip_utf8() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
Doc =
{[
{<<"value">>, <<16#C3, 16#84>>}
]},
- Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[<<16#C3, 16#84>>, <<16#C3, 16#9C>>]]], Result),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_roundtrip_modified_utf8() ->
+should_roundtrip_modified_utf8(_) ->
% Mimicking the test case from the mailing list
Src = <<"
function(doc) {
@@ -132,17 +133,17 @@ should_roundtrip_modified_utf8() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
Doc =
{[
{<<"value">>, <<16#C3, 16#84>>}
]},
- Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result = prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[<<16#C3, 16#A4>>, <<16#C3, 16#9C>>]]], Result),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_replace_broken_utf16() ->
+should_replace_broken_utf16(_) ->
% This test reverse the surrogate pair of
% the Boom emoji U+1F4A5
Src = <<"
@@ -151,12 +152,12 @@ should_replace_broken_utf16() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
Doc =
{[
{<<"value">>, list_to_binary(xmerl_ucs:to_utf8([16#1F4A5]))}
]},
- Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result = prompt(Proc, [<<"map_doc">>, Doc]),
% Invalid UTF-8 gets replaced with the 16#FFFD replacement
% marker
Markers = list_to_binary(xmerl_ucs:to_utf8([16#FFFD, 16#FFFD])),
@@ -164,7 +165,7 @@ should_replace_broken_utf16() ->
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_allow_js_string_mutations() ->
+should_allow_js_string_mutations(_) ->
% This binary corresponds to this string: мама мыла раму
% Which I'm told translates to: "mom was washing the frame"
MomWashedTheFrame = <<
@@ -213,11 +214,11 @@ should_allow_js_string_mutations() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src1]),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src2]),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src3]),
+ true = prompt(Proc, [<<"add_fun">>, Src1]),
+ true = prompt(Proc, [<<"add_fun">>, Src2]),
+ true = prompt(Proc, [<<"add_fun">>, Src3]),
Doc = {[{<<"value">>, MomWashedTheFrame}]},
- Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
+ Result = prompt(Proc, [<<"map_doc">>, Doc]),
Expect = [
[[<<"length">>, 14]],
[[<<"substring">>, Washed]],
@@ -227,7 +228,101 @@ should_allow_js_string_mutations() ->
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
-should_exit_on_oom() ->
+should_bump_timing_stats(_) ->
+ Proc = couch_query_servers:get_os_process(<<"javascript">>),
+ ?assert(sample_time(spawn_proc) > 0),
+
+ ResetTime = sample_time(reset),
+ true = prompt(Proc, [<<"reset">>]),
+ ?assert(sample_time(reset) > ResetTime),
+
+ AddFunTime = sample_time(add_fun),
+ true = prompt(Proc, [
+ <<"add_fun">>,
+ <<"function(doc) {emit(doc.x, doc.y);}">>
+ ]),
+ ?assert(sample_time(add_fun) > AddFunTime),
+
+ MapTime = sample_time(map),
+ [[[1, 2]]] = prompt(Proc, [
+ <<"map_doc">>,
+ {[{<<"x">>, 1}, {<<"y">>, 2}]}
+ ]),
+ ?assert(sample_time(map) > MapTime),
+
+ ReduceTime = sample_time(reduce),
+ [true, [2]] = prompt(Proc, [
+ <<"reduce">>,
+ [<<"function(k, v) {return sum(v);}">>], [[1, 2]]
+ ]),
+ ?assert(sample_time(reduce)> ReduceTime),
+
+ ReduceTime1 = sample_time(reduce),
+ [true, [7]] = prompt(Proc, [
+ <<"rereduce">>,
+ [<<"function(k, v) {return sum(v);}">>], [3, 4]
+ ]),
+ ?assert(sample_time(reduce) > ReduceTime1),
+
+ FilterFun = <<"function(doc, req) {return true;}">>,
+ UpdateFun = <<"function(doc, req) {return [null, 'something'];}">>,
+ VduFun = <<"function(cur, old, ctx, sec) {return true;}">>,
+ DDocId = <<"_design/ddoc1">>,
+ DDoc = #{
+ <<"_id">> => DDocId,
+ <<"_rev">> => <<"1-a">>,
+ <<"filters">> => #{<<"f1">> => FilterFun},
+ <<"updates">> => #{<<"u1">> => UpdateFun},
+ <<"validate_doc_update">> => VduFun
+ },
+
+ NewDDocTime = sample_time(ddoc_new),
+ true = prompt(Proc, [
+ <<"ddoc">>,
+ <<"new">>,
+ DDocId,
+ DDoc
+ ]),
+ ?assert(sample_time(ddoc_new) > NewDDocTime),
+
+ VduTime = sample_time(ddoc_vdu),
+ 1 = prompt(Proc, [
+ <<"ddoc">>,
+ DDocId,
+ [<<"validate_doc_update">>],
+ [#{}, #{}]
+ ]),
+ ?assert(sample_time(ddoc_vdu) > VduTime),
+
+ FilterTime = sample_time(ddoc_filter),
+ [true, [true]] = prompt(Proc, [
+ <<"ddoc">>,
+ DDocId,
+ [<<"filters">>, <<"f1">>],
+ [[#{}], #{}]
+ ]),
+ ?assert(sample_time(ddoc_filter) > FilterTime),
+
+ DDocOtherTime = sample_time(ddoc_other),
+ prompt(Proc, [
+ <<"ddoc">>,
+ DDocId,
+ [<<"updates">>, <<"u1">>],
+ [null, #{}]
+ ]),
+ ?assert(sample_time(ddoc_other) > DDocOtherTime),
+
+ OtherTime = sample_time(other),
+ true = prompt(Proc, [
+ <<"add_lib">>,
+ #{<<"foo">> => <<"exports.bar = 42;">>}
+ ]),
+ ?assert(sample_time(other) > OtherTime),
+
+ couch_query_servers:ret_os_process(Proc).
+
+%% erlfmt-ignore
+should_exit_on_oom(_) ->
Src = <<"
var state = [];
function(doc) {
@@ -239,11 +334,11 @@ should_exit_on_oom() ->
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
trigger_oom(Proc).
%% erlfmt-ignore
-should_exit_on_internal_error() ->
+should_exit_on_internal_error(_) ->
% A different way to trigger OOM which previously used to
% throw an InternalError on SM. Check that we still exit on that
% type of error
@@ -260,11 +355,11 @@ should_exit_on_internal_error() ->
emit(42, j.length);}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
- true = couch_query_servers:proc_prompt(Proc, [<<"reset">>]),
- true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
+ true = prompt(Proc, [<<"reset">>]),
+ true = prompt(Proc, [<<"add_fun">>, Src]),
Doc = {[]},
try
- couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc])
+ prompt(Proc, [<<"map_doc">>, Doc])
catch
% Expect either an internal error thrown if it catches it and
% emits an error log before dying
@@ -285,7 +380,7 @@ should_exit_on_internal_error() ->
trigger_oom(Proc) ->
Status =
try
- couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, <<"{}">>]),
+ prompt(Proc, [<<"map_doc">>, <<"{}">>]),
continue
catch
throw:{os_process_error, {exit_status, 1}} ->
@@ -295,3 +390,9 @@ trigger_oom(Proc) ->
continue -> trigger_oom(Proc);
done -> ok
end.
+
+sample_time(Stat) ->
+ couch_stats:sample([couchdb, query_server, time, Stat]).
+
+prompt(Proc, Cmd) ->
+ couch_query_servers:proc_prompt(Proc, Cmd).