You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@couchdb.apache.org by "chewbranca (via GitHub)" <gi...@apache.org> on 2023/10/17 21:53:34 UTC

[PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

chewbranca opened a new pull request, #4812:
URL: https://github.com/apache/couchdb/pull/4812

   # Couch Stats Resource Tracker Overview
   
   We currently lack visibility into the types of operations that are inducing
   system resource usage in CouchDB. We can see in the IOQ stats the raw
   quantities of IO operations being induced, at categories of essentially "normal
   database reads", "database writes", "view index writes", "internal
   replication", and "compaction". The problem is connecting the dots between the
   data in IOQ and the corresponding database operation types inducing the work,
   and going a step further, connecting that with individual requests to identify
   the actual operations inducing all of the work.
   
   In particular, I believe this is especially pronounced in aggregate
   database read operations that perform a fold over the `id_tree` or `seq_tree` of
   the underlying `.couch` file. We do not currently instrument `couch_btree.erl`
   with `couch_stats` to track operations, so we do not have any data to correlate
   `couch_btree:fold*` operations with the volume of IOQ traffic flowing through
   the system. A basic first step would be to add counters for the amount of btree
   fold reads performed, however, that would still be insufficient as it would not
   allow us to distinguish between `_all_docs`, `_view`, `_find`, and `_changes`
   unless we track which type of fold operation is being induced.
   
   Ideally, we should be able to correlate the overall IOQ throughput with the
   different types of API operations being induced. Beyond that, it would be
   fantastic if we can extend CouchDB's introspection capabilities such that we
   can track the types of operations being performed at the user/db/request level,
   allowing users to understand what operations and requests are utilizing system
   resources.
   
   This writeup proposes an approach to alleviate these introspection blindspots by
   collecting additional cluster level metrics at the RPC operation level providing
   context about the overall volume of requests, the magnitude of data processed,
   and the response payload magnitude. By extending the global stats collection
   logic we can allow for process local tracking of stats and build a system for
   tracking and exposing that data at the RPC level, the node local level, and at
   an HTTP API level. I think it's essential we target cluster level workloads in
   addition to request level so we can correlate resource usage with individual
   requests. Given how variable API operations are in CouchDB (eg single doc read
   reads one doc from 3 shard replicas, whereas a `_find` query failing to find an
   index will do a full database scan _every time_), I think it's important  to be
   able to easily see what types of operations are consuming cluster resources, and
   then readily identify the heavy hitting requests.
   
   This data can be tracked in records in ETS tables allowing for realtime
   introspection of active resource consumption across all operations. We can focus
   on monotonically increasing counters for tracking core operations and avoid an
   assortment of complexity from negative values and dealing with histograms. We
   can leave histograms to other stacks by tracking start/etc/stop timestamps. By
   only focusing on monotonically increasing values as a function of resource
   consumption we can take a delta between any two timestamps and get a precise
   rate of consumption for this process/coordinator during the given time window.
   This also allows us to iteratively track stats between any start and stop point.
   The simple case is from process start to process finish, but anywhere any
   between is a valid delta and we can distinguish between live snapshots and final
   full process workload. By having RPC worker processes store data to the node
   local ETS tables in addition to sending resouce usage deltas in rexi
   replies we can track node local RPC worker resource usage in addition to node
   local coordinator processes and how much work they've induced across the
   cluster. This provides both insight into the active heavy operations in the
   cluster but also provides users with easy access to identify heavy hitter
   requests for further optimization.
   
   By focusing on only tracking monotonically increasing counters about core
   resource usage we can utilize a delta based system allowing for resource
   tracking on the desired stat. Essentially this allows us to create
   `recon:proc_window` but `couchdb:proc_window` and identify the most active
   processes by account or docs reads or IOQ calls or rows read or JS filter
   invocations or by data volume or whatever else we choose to track.
   
   The delta based approach allows us to decouple from any particular time
   interval based collection approach and instead focus on what provides the most
   insight in any particular context. This allows at the base case to collect
   stats time start to time end, but also provides intermediate values at whtaever
   intervals/rates we desire. This also allows us to use the same stats collection
   logic for both streaming and non streaming API endpoints as we can handle full
   deltas or partials. It's key we support delta operations as we need to see long
   running processes live, not when they're completed potentially hours later, so
   delta updates are essential to live tracking on long running operations. (In
   practice, long running `_find` query RPC workers can induce work for
   potentially hours, so it's inappropriate to wait until the worker completes to
   stream the resource usage, as otherwise it'll be hours behind when it actually
   happened).
   
   With the idea of using the existing `couch_stats:increment_counter` logic to
   handle resource tracking, we can extend that `increment_counter` logic so that
   in addition to the normal global tracking of the desired metric, we also begin
   tracking a process local set of statistics allowing us so isolate resources
   utilized to individual requests and even rpc workers.
   
   This is straightforward to accomplish from within
   `couch_stats:increment_counter` as we have the exact stats being recorded but
   also we're still in the process that invoked those stats, so this draft PR also
   has the local process performing operations update an ets table entry for its
   own usage levels. This results in all worker processes concurrently updating
   ETS tables in realtime as a function of resource usage. Unlike our normal stats
   collection where we have many processes writing to a small number of keys
   creating lock contention, for process local stats each process only writes to a
   singular key corresponding to the process ID, so there is no overlapping stats
   collection across keys, allowing us to take full advantage of ETS
   `write_concurrency`.
   
   Each process updates in realtime its process usage stats as the normal
   `couch_stats:increment_counter` code paths are exercised. We utilize a
   timestamp based approach to track when each update occurs, and given we're only
   interacting with monotonically increasing counters, we can take the delta
   between any two time points and that will give us a reasonably accurate rate of
   change. This allows us to create the equivalent of `recon:proc_window` but for
   `couch_db:proc_window` to find busiest
   processes/requests/databases/shards/users/etc.
   
   The timestamp delta approach also allows us to send deltas from rpc worker back
   to coordinator during the normally desired `rexi:reply` occurances by embedding
   the resource delta into the rexi reply, allowing us to incrementally send back
   worker usage stats to be aggregated at the coordinator level so we can find
   runaway processes prior to seeing them in the report logs. This approach also
   allows us extend `rexi:ping` with the same logic so we can keep resource usage
   stats streaming independently of when rows are streamed back to the coodinator
   (a major issue we've encountered with long running bad `_find` queries).
   
   This also allows us to create watchdog processes that kill a cluster process
   chain in the event it has surpassed certain thresholds. This would be easily
   achievable at the node local level or coordinator cluster level. We'll also
   likely want some type of watchdog process to clear our old entries in the ETS
   cache. The process being tracked itself shouldn't be accountable for the
   longevity in the stats table, as we want to leave a small buffer of time so we
   don't lose info about the processes the second they exit. This is a common
   problem when using `recon:proc_window` where if you try and `process_info` a
   process you found with `proc_window` it could be dead by the end of the proc
   window, losing any potential insights. By having say a 10 second buffer on
   letting processes remained completed in the stats table we can see more easily
   track and interact with live work.
   
   This system also allows us to generate a per process report of resources
   consumed both for local processes invoked by way of `fabric_rpc` and also
   cluster wide aggregations at the coordinator level allowing for realtime usage
   reporting of active requests utilizing the new report engine. For example:
   
   > [report] 2023-10-06T23:24:35.974642Z node1@127.0.0.1 <0.296.0> -------- [csrt-pid-usage-lifetime changes_processed=0 docs_read=0 mfa="{fabric_rpc,open_doc,3}" nonce="null" pid_ref="<0.1558.0>#Ref<0.1796228611.1272446977.134363>" rows_read=0 state="{down,noproc}" updated_at="{1696,634675,974598}"]
   
   
   # Core Tasks
   
   * extend stats collection into `fabric_rpc` (or `mango_httpd` `dreyfus_rpc`, etc)
     - target core streaming API operations; changes feed as first step
     - each operation should include _at least_ metrics for:
       1) number of rpc invocations
       2) number of documents processed (request magnitude)
       3) number of documents streamed out (response magnitude)
     - for example, changes feed should do:
       1) `couch_stats:increment_counter([fabric_rpc, changes_feed])`
         - on rpc invocation
       2) `couch_stats:increment_counter([fabric_rpc, changes_feed, row_processed])`
         - on rows read; perhaps also for docs read
       3) `couch_stats:increment_counter([fabric_rpc, changes_feed, rows_returned])`
         - when streaming rows back to the client
       4) we should also include JS filter invocations for relevant API operations 
     - somewhat awkward with rows vs docs and needing js filters
     - we'll likely need to play around with what all to collect here, but in
       general we need direct visibility into core operations induced and what is
       performing them; Cluster level metrics give us the former, and reports/real
       time reporting gives us the latter
   
   * Extend `couch_stats` to do local process stat tracking
     - when we do `couch_stats:increment_counter`, do something like
       `maybe_track_local_stat(StatOp)` where we can track local process stats for
       the subset of stats we're interested in having reports and real time stats
       available for
     - this is the key second step from tracking the additional RPC data. Basically
       we start tracking the core data we want so we can use that for cluster
       status introspection and usage levels, then we gather the local stats for
       identifying which requests are heaviest
     - this provides a mechanism to track any additional stats on RPC we want
       without having to immediately introduce new metrics for all RPC operations we
       want, while skipping undesired operations
   
   * Store local stats into ETS backend
     - several counters backend options available. I believe an ETS table (or
       shared set of ETS tables like we do with `couch_server(N)` and others) that
       utilizies a combination of `read_concurrency`, `write_concurrency`, and
       `distributed_counters` will provide us with a performant system that allows
       for isolated tracking of per process stats in a way that allows for
       concurrent writers with no overlapping writers in addition to read
       concurrency and distributed counters to minimize impact of writing to the
       shared table across processes. Each process will be its only writer,
       minimizing concurrency issues, yet we can still do a more expensive read
       operation over the ETS table(s) to allow for aggregate real time tooling to
       expose at the `remsh` and HTTP API (eg `couch_debug` functions for finding
       heaviest requests or similar exposing of data over http to end up in a
       Fauxton interface providing real time stats
     - key off of `{self(), make_ref()}` to ensure uniqueness in stats table
     - also can store:
       - user
       - db/shard
       - request type
       - coordinator vs rpc vs background task
       - these are specifically for introspection and grouping on with ets queries
     - use records for tracking data
       - this allows for use of atomic `ets:update_counter` operations
       - easily interact with particular fields
       - also allows for `ets:match` and `ets:select` to perform aggregate queries
         about overal system load. I think this is only possible with records in
         ETS, I don't think we could use counters and select with maps?
   
   * Extend `rexi:{reply,stream,etc}` to send usage deltas
     - we can take the current stats record for the process and delta with that
       since a `T0` stats record and provide a delta. We we make that delta we
       store the stats at the time of the delta `TDelta1`, then afterwards we 
       perform our deltas against the last record snapshot and then we can continue
       to send deltas over time and still return a full workload usage if desired.
     - key thing here is we need to stream the stats back, we can't wait until the
       RPC worker is done because sometimes that takes hours. Need to be able to get
       iterative stats so we can find problematic processes live
     - need to address related issue where responses coming in from other shards
       are not accounted for in the usage as those responses are dropped, eg when
       limit is reached in Mango find at the coordinator level before shard level
     - alternative is specifically sending stats lines, but I think we'll be far
       better served by always sending deltas with every rpc line
   
   * Introduce watchdog process around stats collection
   
   This has two core jobs:
   
     1) clear out old process stats after desired delay
       - we want to have data about recent processes that just exited to faciliate
         human interaction with busy yet fast lived processes
     2) provide an automated mechanism for killing processes
       - I think this is an important initial feature, at the very least we should
         be able to tell a cluster to kill find requests that are still going after
         an hour, for example.
       - can easily find long running processes or heavy processes using ets:select
       - can also key on user to find most active users
       - I think with a bit of thought and a few configuration options we can make
         this usable for helping out with problematic specific workflows on a per
         user/request basis
       - for example:
         - "automatically kill all find requests that have processed a million docs
           yet returned none"
   
   * Introduce remsh/http level introspection tooling
     - eg make `couchdb:proc_window` and some other tools for filtering and sorting
       the stats collection tables. This tooling should be readily available by way
       of remsh but we should also expose the same introspection APIs over http
       from the get go so that this can be utilized directly by users with curl and
       end up in a nice Fauxton dashboard showing realtime usage levels.
     - this doesn't have to be complicated, but should easily be able to query the
       local rpc node, all cluster rpc nodes, and all cluster coordinators. It
       should be easy to get a cluster level view of overall usage, grouped by the
       desired set of stats.
     - provide tooling to teardown RPC workers and coordinators with full worker
       teardown. Once we make it easy to find the heavy hitters, we need to make it
       easy to take action against them. Obviously being able to kill the processes
       is essential, but I could see providing additional functionality like
       perhaps allowing for lowering the IOQ priority for the given request if it's
       a request that genuinely needs to complete but is impacting the system.
   
   I've got a hacked together http API to demonstrate being able to query the stats
   collection across the cluster directly over http:
   
   <img width="1719" alt="Screenshot 2023-10-06 at 3 29 50 PM" src="https://github.com/apache/couchdb/assets/5326/1aebe0f7-ec8a-4f36-8a0f-f92385d8a96b">
   
   
   # Overall Approach
   
   The key task here is to build out the mechanisms for collecting/tracking stats
   locally, shipping them back over the wire to the coordinator for aggregation,
   handling the deltas, exposing them realtime stats, and then generating reports
   with the data. I believe this to be the core work at hand, where afterwards
   extending out the API operations to collect the data is fairly straightforward;
   for instance, once the functionality is in place, adding changes feed reports
   is simply adding the new stats to collect to the relevant `fabric_rpc`
   functions and then ensuring we've flagged those new stats as desired fields to
   expose in our resource usage tracking. We could easily extend compactions or
   indexing with similar stats and reporting logic. Once we have a system in place
   for tracking process local metrics, sending and aggregating the deltas, then
   exposing the data over querying interfaces, we can easily extend this level of
   introspection to any operations in the system, and in my opinion, we should
   extend this coverage to 100% of database operations such that all resource
   usage is accounted for and tracked.
   
   This is still a bit rough in a few areas, and not everything is fully
   operational, but essentially the core framework is operational and demonstrates
   a working version of what I've detailed out. I believe this to be a viable
   approach and that the proof of concept provides a clear path forward on how to
   deliver this.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449379485


##########
src/rexi/src/rexi_utils.erl:
##########
@@ -100,6 +110,13 @@ process_message(RefList, Keypos, Fun, Acc0, TimeoutRef, PerMsgTO) ->
             end;
         {rexi_DOWN, _, _, _} = Msg ->
             Fun(Msg, nil, Acc0)
-    after PerMsgTO ->
-        {timeout, Acc0}
     end.
+
+extract_delta({A, {delta, Delta}}) -> {{A}, Delta};

Review Comment:
   Why not modify add_delta?
   
   ```
   add_delta(Tuple, Delta) -> {Delta, Tuple};
   ``` 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "pgj (via GitHub)" <gi...@apache.org>.
pgj commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1413110894


##########
src/rexi/src/rexi.erl:
##########
@@ -328,3 +336,23 @@ drain_acks(Count) ->
     after 0 ->
         {ok, Count}
     end.
+
+get_delta() ->
+    {delta, couch_stats_resource_tracker:make_delta()}.
+
+maybe_add_delta(T) ->
+    case couch_stats_resource_tracker:is_enabled() of
+        false ->
+            T;
+        true ->
+            add_delta(T, get_delta())
+    end.
+
+add_delta({A}, Delta) -> {A, Delta};

Review Comment:
   Would not it be better to make it the inverse of `rexi_utils:get_delta/1`?  (And place this function in `rexi_utils` as well?)  That is, the `Delta` argument would not be expected to be like `{delta, D}`, but only `D` and the `delta` tag would be automatically applied on that:
   
   ```erlang
   add_delta(T, Delta) when 0 < erlang:size(T), erlang:size(T) < 8 ->
       erlang:insert_element(erlang:size(T) + 1, T, {delta, Delta});
   add_delta(T, _Delta) -> T.
   ```
   
   This way the following property would hold:
   
   ```erlang
   ?FORALL(T, tuple(),
       ?FORALL(D, any(),
           extract_delta(add_delta(T, D)) == {T, D})).
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449369253


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->
+            M = atom_to_binary(M0),
+            F = atom_to_binary(F0),
+            A = integer_to_binary(A0),
+            <<M/binary, ":", F/binary, "/", A/binary>>;
+        MFA0 when is_list(MFA0) ->
+            MFA0;
+        undefined ->
+            null;
+        OtherMFA ->
+            throw({error, {unexpected, OtherMFA}})
+    end,
+    From = case From0 of
+        {Parent, ParentRef} ->
+            {term_to_flat_json(Parent), term_to_flat_json(ParentRef)};
+        undefined ->
+            null
+    end,
+    State = case State0 of
+        alive ->
+            alive;
+        {down, Reason0}  ->
+            Reason = term_to_flat_json(Reason0),
+            <<"down: ", Reason/binary>>
+    end,
+    Nonce = case Nonce0 of
+        undefined ->
+            null;
+        Nonce0 ->
+            list_to_binary(Nonce0)
+    end,
+    #{
+        updated_at => TP,
+        started_at => TInit,
+        pid_ref => PidRef,
+        mfa => MFA,
+        nonce => Nonce,
+        from => From,
+        dbname => DbName,
+        username => UserName,
+        db_open => DbOpens,
+        docs_read => DocsRead,
+        js_filter => JSFilters,
+        js_filter_error => JSFilterErrors,
+        js_filtered_docs => JSFilteredDocss,
+        rows_read => RowsRead,
+        state => State,
+        type => term_to_flat_json({type, Type}),
+        kp_nodes => KpNodes,
+        kv_nodes => KvNodes,
+        btree_folds => ChangesProcessed,
+        changes_returned => ChangesReturned,
+        ioq_calls => IoqCalls
+    }.
+
+
+convert_mfa({M0, F0, A0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    A = integer_to_binary(A0),
+    <<M/binary, ":", F/binary, "/", A/binary>>;
+convert_mfa(undefined) ->
+    null.
+
+convert_type(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+convert_type({coordinator, Verb0, Path0}) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+convert_type({worker, M0, F0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>.
+
+get_pid_ref() ->
+    case get(?PID_REF) of
+        undefined ->
+            Ref = make_ref(),
+            set_pid_ref({self(), Ref});
+        PidRef ->
+            PidRef
+    end.
+
+
+create_context() ->
+    is_enabled() andalso create_context(self()).
+
+
+create_context(Pid) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            Ref = make_ref(),
+            Rctx = make_record(Pid, Ref),
+            track(Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+
+create_resource(#rctx{} = Rctx) ->
+    %% true = ets:insert(?MODULE, Rctx).
+    catch ets:insert(?MODULE, Rctx).
+
+%% add type to disnguish coordinator vs rpc_worker
+create_context(From, {M,F,_A} = MFA, Nonce) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            PidRef = get_pid_ref(), %% this will instantiate a new PidRef
+            %% TODO: extract user_ctx and db/shard from
+            Rctx = #rctx{
+                pid_ref = PidRef,
+                from = From,
+                mfa = MFA,
+                type = {worker, M, F},
+                nonce = Nonce
+            },
+            track(Rctx),
+            erlang:put(?DELTA_TZ, Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+create_coordinator_context(#httpd{path_parts=Parts} = Req) ->
+    is_enabled() andalso create_coordinator_context(Req, io_lib:format("~p", [Parts])).
+
+create_coordinator_context(#httpd{} = Req, Path) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            #httpd{
+                method = Verb,
+                nonce = Nonce
+            } = Req,
+            PidRef = get_pid_ref(), %% this will instantiate a new PidRef
+            Rctx = #rctx{
+                pid_ref = PidRef,
+                type = {coordinator, Verb, [$/ | Path]},
+                nonce = Nonce
+            },
+            track(Rctx),
+            erlang:put(?DELTA_TZ, Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+set_context_dbname(DbName) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            catch case ets:update_element(?MODULE, get_pid_ref(), [{#rctx.dbname, DbName}]) of

Review Comment:
   These blocks look very similar in all of the functions. Maybe worth extracting out as a function `update_element(Idx, Value)`. Although in this case you would need to remove one line from stacktrace.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449121481


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).

Review Comment:
   we do check `is_enabled` in `updated_counter/3` are you doing a shortcut for performance reasons? If so let's add a comment.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449123298


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).

Review Comment:
   `update_counter/2` implemented in terms of `update_counter/3` which does call `is_enabled()`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1731576347


##########
src/rexi/src/rexi.erl:
##########
@@ -328,3 +336,23 @@ drain_acks(Count) ->
     after 0 ->
         {ok, Count}
     end.
+
+get_delta() ->
+    {delta, couch_stats_resource_tracker:make_delta()}.
+
+maybe_add_delta(T) ->
+    case couch_stats_resource_tracker:is_enabled() of
+        false ->
+            T;
+        true ->
+            add_delta(T, get_delta())
+    end.
+
+add_delta({A}, Delta) -> {A, Delta};

Review Comment:
   There is also https://www.erlang.org/doc/apps/erts/erlang#append_element/2. However in the way it is written there might be an opportunity for structural sharing. Since in this case the new tuple is constructed from existing pointers. I am not sure whether VM would eliminate copying in this case.
   
   I did a quick experiment just to see what gets emited.
    
   ```erlang
   % erlc +time +to_core add_delta.erl
   -module(add_delta).
   
   -export([test/0]).
   
   add_delta0({A}, Delta) -> {A, Delta};
   add_delta0({A, B}, Delta) -> {A, B, Delta}.
   
   add_delta1(T, Delta) when 0 < erlang:size(T), erlang:size(T) < 8 ->
       erlang:insert_element(erlang:size(T) + 1, T, Delta);
   add_delta1(T, _Delta) -> T.
   
   add_delta2(T, Delta) -> erlang:append_element(T, Delta).
   
   test() ->
       {1,2,3, {delta, 4}} = add_delta0({1,2,3}, {delta, 4}),
       {1,2,3, {delta, 4}} = add_delta1({1,2,3}, {delta, 4}),
       {1,2,3, {delta, 4}} = add_delta2({1,2,3}, {delta, 4}),
       ok.
   
   
   ```
   
   The resulting core erlang is
   
   ```
   module 'add_delta' ['module_info'/0,
   		    'module_info'/1,
   		    'test'/0]
       attributes [%% Line 1
   		'file' =
   		    %% Line 1
   		    [{[97|[100|[100|[95|[100|[101|[108|[116|[97|[46|[101|[114|[108]]]]]]]]]]]]],1}]]
   'add_delta0'/2 =
       %% Line 6
       ( fun (_0,_1) ->
   	  ( case <_0,_1> of
   	      <{A},Delta> when 'true' ->
   		  {A,Delta}
   	      %% Line 7
   	      <{A,B},Delta> when 'true' ->
   		  {A,B,Delta}
   	      ( <_3,_2> when 'true' ->
   		    ( primop 'match_fail'
   			  (( {'function_clause',_3,_2}
   			     -| [{'function',{'add_delta0',2}}] ))
   		      -| [{'function',{'add_delta0',2}}] )
   		-| ['compiler_generated'] )
   	    end
   	    -| [{'function',{'add_delta0',2}}] )
         -| [{'function',{'add_delta0',2}}] )
   'add_delta1'/2 =
       %% Line 9
       ( fun (_0,_1) ->
   	  ( case <_0,_1> of
   	      <T,Delta>
   		  when try
   			let <_2> =
   			    call 'erlang':'size'
   				(T)
   			in  let <_3> =
   				call 'erlang':'<'
   				    (0, _2)
   			    in  let <_4> =
   				    call 'erlang':'size'
   					(T)
   				in  let <_5> =
   					call 'erlang':'<'
   					    (_4, 8)
   				    in  call 'erlang':'and'
   					    (_3, _5)
   		    of <Try> ->
   			Try
   		    catch <T,R> ->
   			'false' ->
   		  let <_6> =
   		      call %% Line 10
   			   'erlang':%% Line 10
   				    'size'
   			  (%% Line 10
   			   T)
   		  in  let <_7> =
   			  call %% Line 10
   			       'erlang':%% Line 10
   					'+'
   			      (%% Line 10
   			       _6, %% Line 10
   				   1)
   		      in  %% Line 10
   			  call 'erlang':'insert_element'
   			      (_7, T, Delta)
   	      %% Line 11
   	      <T,_X_Delta> when 'true' ->
   		  T
   	    end
   	    -| [{'function',{'add_delta1',2}}] )
         -| [{'function',{'add_delta1',2}}] )
   'add_delta2'/2 =
       %% Line 13
       ( fun (_0,_1) ->
   	  call 'erlang':'append_element'
   	      (_0, _1)
         -| [{'function',{'add_delta2',2}}] )
   'test'/0 =
       %% Line 15
       ( fun () ->
   	  %% Line 16
   	  case apply 'add_delta0'/2
   		   ({1,2,3}, {'delta',4}) of
   	    <{1,2,3,{'delta',4}}> when 'true' ->
   		%% Line 17
   		case apply 'add_delta1'/2
   			 ({1,2,3}, {'delta',4}) of
   		  <{1,2,3,{'delta',4}}> when 'true' ->
   		      %% Line 18
   		      case apply 'add_delta2'/2
   			       ({1,2,3}, {'delta',4}) of
   			<{1,2,3,{'delta',4}}> when 'true' ->
   			    %% Line 19
   			    'ok'
   			( <_2> when 'true' ->
   			      primop 'match_fail'
   				  ({'badmatch',_2})
   			  -| ['compiler_generated'] )
   		      end
   		  ( <_1> when 'true' ->
   			primop 'match_fail'
   			    ({'badmatch',_1})
   		    -| ['compiler_generated'] )
   		end
   	    ( <_0> when 'true' ->
   		  primop 'match_fail'
   		      ({'badmatch',_0})
   	      -| ['compiler_generated'] )
   	  end
         -| [{'function',{'test',0}}] )
   'module_info'/0 =
       ( fun () ->
   	  call 'erlang':'get_module_info'
   	      ('add_delta')
         -| [{'function',{'module_info',0}}] )
   'module_info'/1 =
       ( fun (_0) ->
   	  call 'erlang':'get_module_info'
   	      ('add_delta', ( _0
   			      -| [{'function',{'module_info',1}}] ))
         -| [{'function',{'module_info',1}}] )
   end
   ```
   
   In the first case there is a direct reuse of tuple elements. Most likely no data copying.
   
   In the second case there are calls to size, construction of a new frame in try/catch and `'erlang':'insert_element'`. AFAIK the `'erlang':'insert_element'`(BIF written in C)  copies the values https://github.com/erlang/otp/blob/1afaa459eaae8d51585acc2a363dd2714b98c9c2/erts/emulator/beam/bif.c#L2866.
   
   In the third case `'erlang':'append_element'` which is a call to BIF written in C. This BIF copies the values https://github.com/erlang/otp/blob/1afaa459eaae8d51585acc2a363dd2714b98c9c2/erts/emulator/beam/bif.c#L2833
   
   I think the way it is written would be the fastest. I think Russell wanted to optimize this code path. If I am right Russell could you add a comment about it. Something like "The tuple match is used to enable subterm sharing and avoid data copying.".
   
   Also there might be a middle ground.
   
   ```erlang
   add_delta({A, B}, Delta) -> {A, B, Delta};
   add_delta({A, B, C}, Delta) -> {A, B, C, Delta};
   add_delta({A, B, C, D}, Delta) -> {A, B, C, D, Delta};
   add_delta({A, B, C, D, E}, Delta) -> {A, B, C, D, E, Delta};
   add_delta({A, B, C, D, E, F}, Delta) -> {A, B, C, D, E, F, Delta};
   add_delta({A, B, C, D, E, F, G}, Delta) -> {A, B, C, D, E, F, G, Delta};
   add_delta(T, Delta) when is_tuple(T) -> erlang:append_element(T, Delta).
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449094671


##########
src/couch/src/couch_db.erl:
##########
@@ -1982,6 +1983,7 @@ increment_stat(#db{options = Options}, Stat, Count) when
 ->
     case lists:member(sys_db, Options) of
         true ->
+            %% TODO: we shouldn't leak resource usage just because it's a sys_db

Review Comment:
   Hrmm. How we can do it? Are you planing to store `IS_SYSTEM_DB` key on process dictionary or something?
   
   Final solution to this question is out of scope for the first PR. TODO is good enough for first version of the feature.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "pgj (via GitHub)" <gi...@apache.org>.
pgj commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1413110894


##########
src/rexi/src/rexi.erl:
##########
@@ -328,3 +336,23 @@ drain_acks(Count) ->
     after 0 ->
         {ok, Count}
     end.
+
+get_delta() ->
+    {delta, couch_stats_resource_tracker:make_delta()}.
+
+maybe_add_delta(T) ->
+    case couch_stats_resource_tracker:is_enabled() of
+        false ->
+            T;
+        true ->
+            add_delta(T, get_delta())
+    end.
+
+add_delta({A}, Delta) -> {A, Delta};

Review Comment:
   Would not it be better to make it the inverse of `rexi_utils:get_delta/1`?  (And place this function in `rexi_utils` as well?)  That is, the `Delta` argument would not be expected to be like `{delta, D}`, but only `D` and the `delta` tag would be automatically applied on that:
   
   ```erlang
   add_delta(T, Delta) when 0 < erlang:size(T), erlang:size(T) < 8 ->
       erlang:insert_element(erlang:size(T) + 1, T, {delta, Delta});
   add_delta(T, _Delta) -> T.
   ```
   
   This way the following property would hold:
   
   ```erlang
   ?FORALL(T, tuple(),
       ?FORALL(D, any(),
           ?IMPLIES(0 < erlang:size(T) andalso erlang:size(T) < 8,
               extract_delta(add_delta(T, D)) == {T, D}))).
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449365915


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->
+            M = atom_to_binary(M0),
+            F = atom_to_binary(F0),
+            A = integer_to_binary(A0),
+            <<M/binary, ":", F/binary, "/", A/binary>>;
+        MFA0 when is_list(MFA0) ->
+            MFA0;
+        undefined ->
+            null;
+        OtherMFA ->
+            throw({error, {unexpected, OtherMFA}})
+    end,
+    From = case From0 of
+        {Parent, ParentRef} ->
+            {term_to_flat_json(Parent), term_to_flat_json(ParentRef)};
+        undefined ->
+            null
+    end,
+    State = case State0 of
+        alive ->
+            alive;
+        {down, Reason0}  ->
+            Reason = term_to_flat_json(Reason0),
+            <<"down: ", Reason/binary>>
+    end,
+    Nonce = case Nonce0 of
+        undefined ->
+            null;
+        Nonce0 ->
+            list_to_binary(Nonce0)
+    end,
+    #{
+        updated_at => TP,
+        started_at => TInit,
+        pid_ref => PidRef,
+        mfa => MFA,
+        nonce => Nonce,
+        from => From,
+        dbname => DbName,
+        username => UserName,
+        db_open => DbOpens,
+        docs_read => DocsRead,
+        js_filter => JSFilters,
+        js_filter_error => JSFilterErrors,
+        js_filtered_docs => JSFilteredDocss,
+        rows_read => RowsRead,
+        state => State,
+        type => term_to_flat_json({type, Type}),
+        kp_nodes => KpNodes,
+        kv_nodes => KvNodes,
+        btree_folds => ChangesProcessed,
+        changes_returned => ChangesReturned,
+        ioq_calls => IoqCalls
+    }.
+
+
+convert_mfa({M0, F0, A0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    A = integer_to_binary(A0),
+    <<M/binary, ":", F/binary, "/", A/binary>>;
+convert_mfa(undefined) ->
+    null.
+
+convert_type(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+convert_type({coordinator, Verb0, Path0}) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+convert_type({worker, M0, F0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>.
+
+get_pid_ref() ->
+    case get(?PID_REF) of
+        undefined ->
+            Ref = make_ref(),
+            set_pid_ref({self(), Ref});
+        PidRef ->
+            PidRef
+    end.
+
+
+create_context() ->
+    is_enabled() andalso create_context(self()).
+
+
+create_context(Pid) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            Ref = make_ref(),
+            Rctx = make_record(Pid, Ref),
+            track(Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+
+create_resource(#rctx{} = Rctx) ->
+    %% true = ets:insert(?MODULE, Rctx).
+    catch ets:insert(?MODULE, Rctx).
+
+%% add type to disnguish coordinator vs rpc_worker
+create_context(From, {M,F,_A} = MFA, Nonce) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            PidRef = get_pid_ref(), %% this will instantiate a new PidRef
+            %% TODO: extract user_ctx and db/shard from
+            Rctx = #rctx{
+                pid_ref = PidRef,
+                from = From,
+                mfa = MFA,
+                type = {worker, M, F},
+                nonce = Nonce
+            },
+            track(Rctx),
+            erlang:put(?DELTA_TZ, Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+create_coordinator_context(#httpd{path_parts=Parts} = Req) ->
+    is_enabled() andalso create_coordinator_context(Req, io_lib:format("~p", [Parts])).
+
+create_coordinator_context(#httpd{} = Req, Path) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            #httpd{
+                method = Verb,
+                nonce = Nonce
+            } = Req,
+            PidRef = get_pid_ref(), %% this will instantiate a new PidRef
+            Rctx = #rctx{
+                pid_ref = PidRef,
+                type = {coordinator, Verb, [$/ | Path]},
+                nonce = Nonce
+            },
+            track(Rctx),
+            erlang:put(?DELTA_TZ, Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+set_context_dbname(DbName) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            catch case ets:update_element(?MODULE, get_pid_ref(), [{#rctx.dbname, DbName}]) of
+                false ->

Review Comment:
   ```
   (node1@127.0.0.1)25> element(2, erlang:process_info(self(), current_stacktrace)).
   [{erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,748}]},
    {erl_eval,expr_list,7,[{file,"erl_eval.erl"},{line,961}]},
    {erl_eval,expr,6,[{file,"erl_eval.erl"},{line,465}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,693}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,649}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,634}]}]
   ```
   
   ```
   (node1@127.0.0.1)26> Stack = try 12 / 0 catch Error:Reason:Stacktrace -> Stacktrace end.
   [{erlang,'/',
            [12,0],
            [{error_info,#{module => erl_erts_errors}}]},
    {erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,748}]},
    {erl_eval,try_clauses,10,[{file,"erl_eval.erl"},{line,987}]},
    {erl_eval,expr,6,[{file,"erl_eval.erl"},{line,492}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,693}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,649}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,634}]}]
   ```
   
   I think this is a debugging leftover. The stacktrace produced by `process_info` should be almost the same as from `try/catch`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449356356


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->
+            M = atom_to_binary(M0),
+            F = atom_to_binary(F0),
+            A = integer_to_binary(A0),
+            <<M/binary, ":", F/binary, "/", A/binary>>;
+        MFA0 when is_list(MFA0) ->
+            MFA0;
+        undefined ->
+            null;
+        OtherMFA ->
+            throw({error, {unexpected, OtherMFA}})
+    end,
+    From = case From0 of
+        {Parent, ParentRef} ->
+            {term_to_flat_json(Parent), term_to_flat_json(ParentRef)};
+        undefined ->
+            null
+    end,
+    State = case State0 of
+        alive ->
+            alive;
+        {down, Reason0}  ->
+            Reason = term_to_flat_json(Reason0),
+            <<"down: ", Reason/binary>>
+    end,
+    Nonce = case Nonce0 of
+        undefined ->
+            null;
+        Nonce0 ->
+            list_to_binary(Nonce0)
+    end,
+    #{
+        updated_at => TP,
+        started_at => TInit,
+        pid_ref => PidRef,
+        mfa => MFA,
+        nonce => Nonce,
+        from => From,
+        dbname => DbName,
+        username => UserName,
+        db_open => DbOpens,
+        docs_read => DocsRead,
+        js_filter => JSFilters,
+        js_filter_error => JSFilterErrors,
+        js_filtered_docs => JSFilteredDocss,
+        rows_read => RowsRead,
+        state => State,
+        type => term_to_flat_json({type, Type}),
+        kp_nodes => KpNodes,
+        kv_nodes => KvNodes,
+        btree_folds => ChangesProcessed,
+        changes_returned => ChangesReturned,
+        ioq_calls => IoqCalls
+    }.
+
+
+convert_mfa({M0, F0, A0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    A = integer_to_binary(A0),
+    <<M/binary, ":", F/binary, "/", A/binary>>;
+convert_mfa(undefined) ->
+    null.
+
+convert_type(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+convert_type({coordinator, Verb0, Path0}) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+convert_type({worker, M0, F0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>.
+
+get_pid_ref() ->
+    case get(?PID_REF) of
+        undefined ->
+            Ref = make_ref(),
+            set_pid_ref({self(), Ref});
+        PidRef ->
+            PidRef
+    end.
+
+
+create_context() ->
+    is_enabled() andalso create_context(self()).

Review Comment:
   `is_enabled()` is called in `create_context/1`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1731600337


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,

Review Comment:
   I did an experiment
   
   ```erlang
   % erlc +time +to_core add_delta.erl
   -module(ets_select).
   -include_lib("stdlib/include/ms_transform.hrl").
   
   -export([test/0]).
   
   
   %% TODO: switch to:
   %% -record(?RCTX, {
   -record(rctx, {
       %% Metadata
       started_at,
       updated_at,
       exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
       pid_ref,
       mon_ref,
       mfa,
       nonce,
       from,
       type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
       state = alive,
       dbname,
       username,
   
       %% Stats counters
       db_open = 0,
       docs_read = 0,
       rows_read = 0,
       btree_folds = 0,
       changes_processed = 0,
       changes_returned = 0,
       ioq_calls = 0,
       io_bytes_read = 0,
       io_bytes_written = 0,
       js_evals = 0,
       js_filter = 0,
       js_filter_error = 0,
       js_filtered_docs = 0,
       mango_eval_match = 0,
       %% TODO: switch record definitions to be macro based, eg:
       %% ?COUCH_BT_GET_KP_NODE = 0,
       get_kv_node = 0,
       get_kp_node = 0
   }).
   
   select() ->
       ets:select(?MODULE, ets:fun2ms(fun(#rctx{type = {worker, _, _}} = R) -> R end)).
   
   test() ->
       (catch ets:new(?MODULE, [named_table, {keypos, #rctx.started_at}])),
       true = ets:insert(?MODULE, [#rctx{started_at = 1, type = {worker, 1, 2}}]),
       true = ets:insert(?MODULE, [#rctx{started_at = 2, type = {other, 1, 2}}]),
       io:format("selected = ~p~n", [select()]),
       io:format("all = ~p~n", [ets:tab2list(?MODULE)]).
   ```
   
   The output.
   
   ```
   27> c(ets_select).
   {ok,ets_select}
   28> ets_select:test().
   selected = [{rctx,1,undefined,undefined,undefined,undefined,undefined,
                     undefined,undefined,
                     {worker,1,2},
                     alive,undefined,undefined,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}]
   all = [{rctx,1,undefined,undefined,undefined,undefined,undefined,undefined,
                undefined,
                {worker,1,2},
                alive,undefined,undefined,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
          {rctx,2,undefined,undefined,undefined,undefined,undefined,undefined,
                undefined,
                {other,1,2},
                alive,undefined,undefined,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}]
   ok
   29>
   BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
          (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution
   ```
   
   The core erlang generated by compiler
   
   ```
   module 'ets_select' ['module_info'/0,
   		     'module_info'/1,
   		     'test'/0]
       attributes ['compile' =
   		    [{'nowarn_unused_record',[['rctx']]}],
   		%% Line 1
   		'file' =
   		    %% Line 1
   		    [{[101|[116|[115|[95|[115|[101|[108|[101|[99|[116|[46|[101|[114|[108]]]]]]]]]]]]]],1}],
   		%% Line 1
   		'file' =
   		    %% Line 1
   		    [{[47|[85|[115|[101|[114|[115|[47|[105|[105|[108|[121|[97|[107|[47|[46|[97|[115|[100|[102|[47|[105|[110|[115|[116|[97|[108|[108|[115|[47|[101|[114|[108|[97|[110|[103|[47|[50|[53|[46|[51|[46|[50|[46|[57|[47|[108|[105|[98|[47|[115|[116|[100|[108|[105|[98|[45|[52|[46|[51|[46|[49|[46|[51|[47|[105|[110|[99|[108|[117|[100|[101|[47|[109|[115|[95|[116|[114|[97|[110|[115|[102|[111|[114|[109|[46|[104|[114|[108]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],1}],
   		%% Line 4
   		'file' =
   		    %% Line 4
   		    [{[101|[116|[115|[95|[115|[101|[108|[101|[99|[116|[46|[101|[114|[108]]]]]]]]]]]]]],4}],
   		%% Line 26
   		'record' =
   		    %% Line 26
   		    [{'rctx',[{'record_field',{28,5},{'atom',{28,5},'started_at'}}|[{'record_field',{29,5},{'atom',{29,5},'updated_at'}}|[{'record_field',{30,5},{'atom',{30,5},'exited_at'}}|[{'record_field',{31,5},{'atom',{31,5},'pid_ref'}}|[{'record_field',{32,5},{'atom',{32,5},'mon_ref'}}|[{'record_field',{33,5},{'atom',{33,5},'mfa'}}|[{'record_field',{34,5},{'atom',{34,5},'nonce'}}|[{'record_field',{35,5},{'atom',{35,5},'from'}}|[{'record_field',{36,5},{'atom',{36,5},'type'},{'atom',{36,12},'unknown'}}|[{'record_field',{37,5},{'atom',{37,5},'state'},{'atom',{37,13},'alive'}}|[{'record_field',{38,5},{'atom',{38,5},'dbname'}}|[{'record_field',{39,5},{'atom',{39,5},'username'}}|[{'record_field',{42,5},{'atom',{42,5},'db_open'},{'integer',{42,15},0}}|[{'record_field',{43,5},{'atom',{43,5},'docs_read'},{'integer',{43,17},0}}|[{'record_field',{44,5},{'atom',{44,5},'rows_read'},{'integer',{44,17},0}}|[{'record_field',{45,5},{'atom',{45,5},'btree_folds'},{'integer',{45,19},0}}|[{'record_field',{46,5
 },{'atom',{46,5},'changes_processed'},{'integer',{46,25},0}}|[{'record_field',{47,5},{'atom',{47,5},'changes_returned'},{'integer',{47,24},0}}|[{'record_field',{48,5},{'atom',{48,5},'ioq_calls'},{'integer',{48,17},0}}|[{'record_field',{49,5},{'atom',{49,5},'io_bytes_read'},{'integer',{49,21},0}}|[{'record_field',{50,5},{'atom',{50,5},'io_bytes_written'},{'integer',{50,24},0}}|[{'record_field',{51,5},{'atom',{51,5},'js_evals'},{'integer',{51,16},0}}|[{'record_field',{52,5},{'atom',{52,5},'js_filter'},{'integer',{52,17},0}}|[{'record_field',{53,5},{'atom',{53,5},'js_filter_error'},{'integer',{53,23},0}}|[{'record_field',{54,5},{'atom',{54,5},'js_filtered_docs'},{'integer',{54,24},0}}|[{'record_field',{55,5},{'atom',{55,5},'mango_eval_match'},{'integer',{55,24},0}}|[{'record_field',{58,5},{'atom',{58,5},'get_kv_node'},{'integer',{58,19},0}}|[{'record_field',{59,5},{'atom',{59,5},'get_kp_node'},{'integer',{59,19},0}}]]]]]]]]]]]]]]]]]]]]]]]]]]]]}]]
   'select'/0 =
       %% Line 62
       ( fun () ->
   	  %% Line 63
   	  call 'ets':'select'
   	      ('ets_select', [{{'rctx','_','_','_','_','_','_','_','_',{'worker','_','_'},'_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_'},[],['$_']}])
         -| [{'function',{'select',0}}] )
   'test'/0 =
       %% Line 65
       ( fun () ->
   	  do  try
   		  %% Line 66
   		  call 'ets':'new'
   		      ('ets_select', ['named_table'|[{'keypos',2}]])
   	      of <_catch_value> ->
   		  _catch_value
   	      catch <Class,Reason,Stk> ->
   		  'ok'
   	      %% Line 67
   	      case call 'ets':'insert'
   		       ('ets_select', [{'rctx',1,'undefined','undefined','undefined','undefined','undefined','undefined','undefined',{'worker',1,2},'alive','undefined','undefined',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}]) of
   		<'true'> when 'true' ->
   		    %% Line 68
   		    case call 'ets':'insert'
   			     ('ets_select', [{'rctx',2,'undefined','undefined','undefined','undefined','undefined','undefined','undefined',{'other',1,2},'alive','undefined','undefined',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}]) of
   		      <'true'> when 'true' ->
   			  let <_2> =
   			      apply %% Line 69
   				    'select'/0
   				  ()
   			  in  do  %% Line 69
   				  call 'io':'format'
   				      ([115|[101|[108|[101|[99|[116|[101|[100|[32|[61|[32|[126|[112|[126|[110]]]]]]]]]]]]]]], [_2|[]])
   				  let <_3> =
   				      call %% Line 70
   					   'ets':%% Line 70
   						 'tab2list'
   					  (%% Line 70
   					   'ets_select')
   				  in  %% Line 70
   				      call 'io':'format'
   					  ([97|[108|[108|[32|[61|[32|[126|[112|[126|[110]]]]]]]]]], [_3|[]])
   		      ( <_1> when 'true' ->
   			    primop 'match_fail'
   				({'badmatch',_1})
   			-| ['compiler_generated'] )
   		    end
   		( <_0> when 'true' ->
   		      primop 'match_fail'
   			  ({'badmatch',_0})
   		  -| ['compiler_generated'] )
   	      end
         -| [{'function',{'test',0}}] )
   'module_info'/0 =
       ( fun () ->
   	  call 'erlang':'get_module_info'
   	      ('ets_select')
         -| [{'function',{'module_info',0}}] )
   'module_info'/1 =
       ( fun (_0) ->
   	  call 'erlang':'get_module_info'
   	      ('ets_select', ( _0
   			       -| [{'function',{'module_info',1}}] ))
         -| [{'function',{'module_info',1}}] )
   end
   ```
   
   As you can see the magic syntax get expanded
   ```
   call 'ets':'select'
   	      ('ets_select', [{{'rctx','_','_','_','_','_','_','_','_',{'worker','_','_'},'_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_'},[],['$_']}])
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449149148


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,

Review Comment:
   This would only work with `-include_lib("stdlib/include/ms_transform.hrl")`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449169955


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->

Review Comment:
   ```
   {_, _, _} -> convert_mfa(MFA1)
   ```
   
   also do we want guards `is_atom(M)`, `is_atom(F)`, `is_integer(A)`, `A > 0`?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449357696


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->
+            M = atom_to_binary(M0),
+            F = atom_to_binary(F0),
+            A = integer_to_binary(A0),
+            <<M/binary, ":", F/binary, "/", A/binary>>;
+        MFA0 when is_list(MFA0) ->
+            MFA0;
+        undefined ->
+            null;
+        OtherMFA ->
+            throw({error, {unexpected, OtherMFA}})
+    end,
+    From = case From0 of
+        {Parent, ParentRef} ->
+            {term_to_flat_json(Parent), term_to_flat_json(ParentRef)};
+        undefined ->
+            null
+    end,
+    State = case State0 of
+        alive ->
+            alive;
+        {down, Reason0}  ->
+            Reason = term_to_flat_json(Reason0),
+            <<"down: ", Reason/binary>>
+    end,
+    Nonce = case Nonce0 of
+        undefined ->
+            null;
+        Nonce0 ->
+            list_to_binary(Nonce0)
+    end,
+    #{
+        updated_at => TP,
+        started_at => TInit,
+        pid_ref => PidRef,
+        mfa => MFA,
+        nonce => Nonce,
+        from => From,
+        dbname => DbName,
+        username => UserName,
+        db_open => DbOpens,
+        docs_read => DocsRead,
+        js_filter => JSFilters,
+        js_filter_error => JSFilterErrors,
+        js_filtered_docs => JSFilteredDocss,
+        rows_read => RowsRead,
+        state => State,
+        type => term_to_flat_json({type, Type}),
+        kp_nodes => KpNodes,
+        kv_nodes => KvNodes,
+        btree_folds => ChangesProcessed,
+        changes_returned => ChangesReturned,
+        ioq_calls => IoqCalls
+    }.
+
+
+convert_mfa({M0, F0, A0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    A = integer_to_binary(A0),
+    <<M/binary, ":", F/binary, "/", A/binary>>;
+convert_mfa(undefined) ->
+    null.
+
+convert_type(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+convert_type({coordinator, Verb0, Path0}) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+convert_type({worker, M0, F0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>.
+
+get_pid_ref() ->
+    case get(?PID_REF) of
+        undefined ->
+            Ref = make_ref(),
+            set_pid_ref({self(), Ref});
+        PidRef ->
+            PidRef
+    end.
+
+
+create_context() ->
+    is_enabled() andalso create_context(self()).
+
+
+create_context(Pid) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            Ref = make_ref(),
+            Rctx = make_record(Pid, Ref),
+            track(Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+
+create_resource(#rctx{} = Rctx) ->
+    %% true = ets:insert(?MODULE, Rctx).
+    catch ets:insert(?MODULE, Rctx).
+
+%% add type to disnguish coordinator vs rpc_worker
+create_context(From, {M,F,_A} = MFA, Nonce) ->
+    case is_enabled() of
+        false ->
+            ok;
+        true ->
+            PidRef = get_pid_ref(), %% this will instantiate a new PidRef
+            %% TODO: extract user_ctx and db/shard from
+            Rctx = #rctx{
+                pid_ref = PidRef,
+                from = From,
+                mfa = MFA,
+                type = {worker, M, F},
+                nonce = Nonce
+            },
+            track(Rctx),
+            erlang:put(?DELTA_TZ, Rctx),
+            create_resource(Rctx),
+            Rctx
+    end.
+
+create_coordinator_context(#httpd{path_parts=Parts} = Req) ->
+    is_enabled() andalso create_coordinator_context(Req, io_lib:format("~p", [Parts])).

Review Comment:
   `is_enabled/0` is called in `create_coordinator_context/2`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1448949767


##########
src/chttpd/src/chttpd.erl:
##########
@@ -323,6 +324,9 @@ handle_request_int(MochiReq) ->
     % Save client socket so that it can be monitored for disconnects
     chttpd_util:mochiweb_client_req_set(MochiReq),
 
+    %% This is probably better in before_request, but having Path is nice

Review Comment:
   Here is fine IMO



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1448960139


##########
src/chttpd/test/eunit/chttpd_db_doc_size_tests.erl:
##########
@@ -25,7 +25,7 @@
 setup() ->
     Hashed = couch_passwords:hash_admin_password(?PASS),
     ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist = false),
-    ok = config:set("couchdb", "max_document_size", "50"),
+    ok = config:set("couchdb", "max_document_size", "50", false),

Review Comment:
   `_Persist = false` for consistency. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449156993


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->
+            M = atom_to_binary(M0),
+            F = atom_to_binary(F0),
+            A = integer_to_binary(A0),
+            <<M/binary, ":", F/binary, "/", A/binary>>;
+        MFA0 when is_list(MFA0) ->
+            MFA0;
+        undefined ->
+            null;
+        OtherMFA ->
+            throw({error, {unexpected, OtherMFA}})
+    end,
+    From = case From0 of
+        {Parent, ParentRef} ->
+            {term_to_flat_json(Parent), term_to_flat_json(ParentRef)};
+        undefined ->
+            null
+    end,
+    State = case State0 of
+        alive ->
+            alive;
+        {down, Reason0}  ->
+            Reason = term_to_flat_json(Reason0),
+            <<"down: ", Reason/binary>>
+    end,
+    Nonce = case Nonce0 of
+        undefined ->
+            null;
+        Nonce0 ->
+            list_to_binary(Nonce0)
+    end,
+    #{
+        updated_at => TP,
+        started_at => TInit,
+        pid_ref => PidRef,
+        mfa => MFA,
+        nonce => Nonce,
+        from => From,
+        dbname => DbName,
+        username => UserName,
+        db_open => DbOpens,
+        docs_read => DocsRead,
+        js_filter => JSFilters,
+        js_filter_error => JSFilterErrors,
+        js_filtered_docs => JSFilteredDocss,
+        rows_read => RowsRead,
+        state => State,
+        type => term_to_flat_json({type, Type}),
+        kp_nodes => KpNodes,
+        kv_nodes => KvNodes,
+        btree_folds => ChangesProcessed,
+        changes_returned => ChangesReturned,
+        ioq_calls => IoqCalls
+    }.
+
+
+convert_mfa({M0, F0, A0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    A = integer_to_binary(A0),
+    <<M/binary, ":", F/binary, "/", A/binary>>;
+convert_mfa(undefined) ->

Review Comment:
   should we handle a case when it is something else.
   
   ```
   convert_mfa(Else) -> 
       list_to_binary(lists:flatten(io_lib:format("{unexpected, ~p}", [Else]))).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449161928


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first

Review Comment:
   👍 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449165610


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->

Review Comment:
   Why not collapse these three cases into one
   
   ```erlang
   term_to_flat_json({type, Type}=_Type) -> convert_type(Atom);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449157765


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {worker,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+field(#rctx{pid_ref=Val}, pid_ref) -> Val;
+%% NOTE: Pros and cons to doing these convert functions here
+%% Ideally, this would be done later so as to prefer the core data structures
+%% as long as possible, but we currently need the output of this function to
+%% be jiffy:encode'able. The tricky bit is dynamically encoding the group_by
+%% structure provided by the caller of *_by aggregator functions below.
+%% For now, we just always return jiffy:encode'able data types.
+field(#rctx{mfa=Val}, mfa) -> convert_mfa(Val);
+field(#rctx{nonce=Val}, nonce) -> Val;
+field(#rctx{from=Val}, from) -> Val;
+field(#rctx{type=Val}, type) -> convert_type(Val);
+field(#rctx{state=Val}, state) -> Val;
+field(#rctx{dbname=Val}, dbname) -> Val;
+field(#rctx{username=Val}, username) -> Val;
+field(#rctx{db_open=Val}, db_open) -> Val;
+field(#rctx{docs_read=Val}, docs_read) -> Val;
+field(#rctx{rows_read=Val}, rows_read) -> Val;
+field(#rctx{btree_folds=Val}, btree_folds) -> Val;
+field(#rctx{changes_processed=Val}, changes_processed) -> Val;
+field(#rctx{changes_returned=Val}, changes_returned) -> Val;
+field(#rctx{ioq_calls=Val}, ioq_calls) -> Val;
+field(#rctx{io_bytes_read=Val}, io_bytes_read) -> Val;
+field(#rctx{io_bytes_written=Val}, io_bytes_written) -> Val;
+field(#rctx{js_evals=Val}, js_evals) -> Val;
+field(#rctx{js_filter=Val}, js_filter) -> Val;
+field(#rctx{js_filter_error=Val}, js_filter_error) -> Val;
+field(#rctx{js_filtered_docs=Val}, js_filtered_docs) -> Val;
+field(#rctx{mango_eval_match=Val}, mango_eval_match) -> Val;
+field(#rctx{get_kv_node=Val}, get_kv_node) -> Val;
+field(#rctx{get_kp_node=Val}, get_kp_node) -> Val.
+
+
+curry_field(Field) ->
+    fun(Ele) -> field(Ele, Field) end.
+
+
+count_by(KeyFun) ->
+    group_by(KeyFun, fun(_) -> 1 end).
+
+
+group_by(KeyFun, ValFun) ->
+    group_by(KeyFun, ValFun, fun erlang:'+'/2).
+
+
+group_by(KeyFun, ValFun, AggFun) ->
+    group_by(KeyFun, ValFun, AggFun, fun ets:foldl/3).
+
+
+%% eg: group_by(mfa, docs_read).
+%% eg: group_by(fun(#rctx{mfa=MFA,docs_read=DR}) -> {MFA, DR} end, ioq_calls).
+%% eg: ^^ or: group_by([mfa, docs_read], ioq_calls).
+%% eg: group_by([username, dbname, mfa], docs_read).
+%% eg: group_by([username, dbname, mfa], ioq_calls).
+%% eg: group_by([username, dbname, mfa], js_filters).
+group_by(KeyL, ValFun, AggFun, Fold) when is_list(KeyL) ->
+    KeyFun = fun(Ele) -> list_to_tuple([field(Ele, Key) || Key <- KeyL]) end,
+    group_by(KeyFun, ValFun, AggFun, Fold);
+group_by(Key, ValFun, AggFun, Fold) when is_atom(Key) ->
+    group_by(curry_field(Key), ValFun, AggFun, Fold);
+group_by(KeyFun, Val, AggFun, Fold) when is_atom(Val) ->
+    group_by(KeyFun, curry_field(Val), AggFun, Fold);
+group_by(KeyFun, ValFun, AggFun, Fold) ->
+    FoldFun = fun(Ele, Acc) ->
+        Key = KeyFun(Ele),
+        Val = ValFun(Ele),
+        CurrVal = maps:get(Key, Acc, 0),
+        NewVal = AggFun(CurrVal, Val),
+        maps:put(Key, NewVal, Acc)
+    end,
+    Fold(FoldFun, #{}, ?MODULE).
+
+
+%% Sorts largest first
+sorted(Map) when is_map(Map) ->
+    lists:sort(fun({_K1, A}, {_K2, B}) -> B < A end, maps:to_list(Map)).
+
+
+%% eg: sorted_by([username, dbname, mfa], ioq_calls)
+%% eg: sorted_by([dbname, mfa], doc_reads)
+sorted_by(KeyFun) -> sorted(count_by(KeyFun)).
+sorted_by(KeyFun, ValFun) -> sorted(group_by(KeyFun, ValFun)).
+sorted_by(KeyFun, ValFun, AggFun) -> sorted(group_by(KeyFun, ValFun, AggFun)).
+
+
+term_to_flat_json({shutdown, Reason0}) when is_atom(Reason0) ->
+    Reason = atom_to_binary(Reason0),
+    <<"shutdown: ", Reason/binary>>;
+term_to_flat_json({type, Atom}) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json({type, {coordinator, Verb0, Path0}}=_Type) ->
+    Verb = atom_to_binary(Verb0),
+    Path = list_to_binary(Path0),
+    <<"coordinator:", Verb/binary, ":", Path/binary>>;
+term_to_flat_json({type, {worker, M0, F0}}=_Type) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    <<"worker:", M/binary, ":", F/binary>>;
+term_to_flat_json(Tuple) when is_tuple(Tuple) ->
+    erlang:tuple_to_list(Tuple);
+term_to_flat_json(Pid) when is_pid(Pid) ->
+    ?l2b(pid_to_list(Pid));
+term_to_flat_json(Ref) when is_reference(Ref) ->
+    ?l2b(ref_to_list(Ref));
+term_to_flat_json(Atom) when is_atom(Atom) ->
+    atom_to_binary(Atom);
+term_to_flat_json(undefined) ->
+    null;
+term_to_flat_json(null) ->
+    null;
+term_to_flat_json(T) ->
+    T.
+
+to_flat_json(#rctx{}=Rctx) ->
+    #rctx{
+        updated_at = TP,
+        started_at = TInit,
+        pid_ref = {Pid, Ref},
+        mfa = MFA0,
+        nonce = Nonce0,
+        from = From0,
+        dbname = DbName,
+        username = UserName,
+        db_open = DbOpens,
+        docs_read = DocsRead,
+        rows_read = RowsRead,
+        js_filter = JSFilters,
+        js_filter_error = JSFilterErrors,
+        js_filtered_docs = JSFilteredDocss,
+        state = State0,
+        type = Type,
+        get_kp_node = KpNodes,
+        get_kv_node = KvNodes,
+        btree_folds = ChangesProcessed,
+        changes_returned = ChangesReturned,
+        ioq_calls = IoqCalls
+    } = Rctx,
+    PidRef = {term_to_flat_json(Pid), term_to_flat_json(Ref)},
+    MFA = case MFA0 of
+        {M0, F0, A0} ->
+            M = atom_to_binary(M0),
+            F = atom_to_binary(F0),
+            A = integer_to_binary(A0),
+            <<M/binary, ":", F/binary, "/", A/binary>>;
+        MFA0 when is_list(MFA0) ->
+            MFA0;
+        undefined ->
+            null;
+        OtherMFA ->
+            throw({error, {unexpected, OtherMFA}})
+    end,
+    From = case From0 of
+        {Parent, ParentRef} ->
+            {term_to_flat_json(Parent), term_to_flat_json(ParentRef)};
+        undefined ->
+            null
+    end,
+    State = case State0 of
+        alive ->
+            alive;
+        {down, Reason0}  ->
+            Reason = term_to_flat_json(Reason0),
+            <<"down: ", Reason/binary>>
+    end,
+    Nonce = case Nonce0 of
+        undefined ->
+            null;
+        Nonce0 ->
+            list_to_binary(Nonce0)
+    end,
+    #{
+        updated_at => TP,
+        started_at => TInit,
+        pid_ref => PidRef,
+        mfa => MFA,
+        nonce => Nonce,
+        from => From,
+        dbname => DbName,
+        username => UserName,
+        db_open => DbOpens,
+        docs_read => DocsRead,
+        js_filter => JSFilters,
+        js_filter_error => JSFilterErrors,
+        js_filtered_docs => JSFilteredDocss,
+        rows_read => RowsRead,
+        state => State,
+        type => term_to_flat_json({type, Type}),
+        kp_nodes => KpNodes,
+        kv_nodes => KvNodes,
+        btree_folds => ChangesProcessed,
+        changes_returned => ChangesReturned,
+        ioq_calls => IoqCalls
+    }.
+
+
+convert_mfa({M0, F0, A0}) ->
+    M = atom_to_binary(M0),
+    F = atom_to_binary(F0),
+    A = integer_to_binary(A0),
+    <<M/binary, ":", F/binary, "/", A/binary>>;
+convert_mfa(undefined) ->

Review Comment:
   maybe not. Let it crash.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "iilyak (via GitHub)" <gi...@apache.org>.
iilyak commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1449147329


##########
src/couch_stats/src/couch_stats_resource_tracker.erl:
##########
@@ -0,0 +1,1014 @@
+% 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.
+
+-module(couch_stats_resource_tracker).
+
+-behaviour(gen_server).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2
+]).
+
+-export([
+    inc/1, inc/2,
+    maybe_inc/2,
+    get_pid_ref/0,
+    accumulate_delta/1
+]).
+
+-export([
+    create_context/0, create_context/1, create_context/3,
+    create_coordinator_context/1, create_coordinator_context/2,
+    is_enabled/0,
+    get_resource/0,
+    get_resource/1,
+    set_context_dbname/1,
+    set_context_handler_fun/1,
+    set_context_username/1,
+    track/1,
+    should_track/1
+]).
+
+-export([
+    active/0,
+    active_coordinators/0,
+    active_workers/0,
+    find_unmonitored/0
+]).
+
+-export([
+    count_by/1,
+    group_by/2,
+    group_by/3,
+    group_by/4,
+    sorted/1,
+    sorted_by/1,
+    sorted_by/2,
+    sorted_by/3,
+
+    find_by_pid/1,
+
+    unsafe_foldl/3,
+
+    term_to_flat_json/1
+]).
+
+-export([
+    make_delta/0
+]).
+
+%% Singular increment operations
+-export([
+    db_opened/0,
+    doc_read/0,
+    row_read/0,
+    btree_fold/0,
+    ioq_called/0,
+    js_evaled/0,
+    js_filtered/0,
+    js_filtered_error/0,
+    js_filtered_doc/0,
+    mango_match_evaled/0,
+    get_kv_node/0,
+    get_kp_node/0
+]).
+
+%% Plural increment operations
+-export([
+    js_filtered_docs/1,
+    io_bytes_read/1,
+    io_bytes_written/1
+]).
+
+-export([
+    field/2,
+    curry_field/1
+]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+%% Use these for record upgrades over the wire and in ETS tables
+%% TODO: alternatively, just delete these. Currently using a map
+%% for shipping deltas over the wire, avoiding much of the
+%% problem here. We'll likely still need to handle upgrades to
+%% map format over time, so let's decide a course of action here.
+-define(RCTX_V1, rctx_v1).
+-define(RCTX, ?RCTX_V1).
+
+-define(MANGO_EVAL_MATCH, mango_eval_match).
+-define(DB_OPEN_DOC, docs_read).
+-define(DB_OPEN, db_open).
+-define(COUCH_SERVER_OPEN, db_open).
+-define(COUCH_BT_FOLDS, btree_folds).
+-define(COUCH_BT_GET_KP_NODE, get_kp_node).
+-define(COUCH_BT_GET_KV_NODE, get_kv_node).
+-define(COUCH_JS_FILTER, js_filter).
+-define(COUCH_JS_FILTER_ERROR, js_filter_error).
+-define(COUCH_JS_FILTERED_DOCS, js_filtered_docs).
+-define(ROWS_READ, rows_read).
+
+%% TODO: overlap between this and couch btree fold invocations
+%% TODO: need some way to distinguish fols on views vs find vs all_docs
+-define(FRPC_CHANGES_ROW, changes_processed).
+-define(FRPC_CHANGES_RETURNED, changes_returned).
+%%-define(FRPC_CHANGES_ROW, ?ROWS_READ).
+
+%% Module pdict markers
+-define(DELTA_TA, csrt_delta_ta).
+-define(DELTA_TZ, csrt_delta_tz). %% T Zed instead of T0
+-define(PID_REF, csrt_pid_ref). %% track local ID
+
+
+-record(st, {
+    eviction_delay = 10 * 1000, %% How many ms dead processes are visible
+    scan_interval = 2048, %% How regularly to perfom scans
+    tracking = #{} %% track active processes for eventual eviction
+}).
+
+
+%% TODO: switch to:
+%% -record(?RCTX, {
+-record(rctx, {
+    %% Metadata
+    started_at = tnow(),
+    updated_at = tnow(),
+    exited_at, %% TODO: do we need a final exit time and additional update times afterwards?
+    pid_ref,
+    mon_ref,
+    mfa,
+    nonce,
+    from,
+    type = unknown, %% unknown/background/system/rpc/coordinator/fabric_rpc/etc_rpc/etc
+    state = alive,
+    dbname,
+    username,
+
+    %% Stats counters
+    db_open = 0,
+    docs_read = 0,
+    rows_read = 0,
+    btree_folds = 0,
+    changes_processed = 0,
+    changes_returned = 0,
+    ioq_calls = 0,
+    io_bytes_read = 0,
+    io_bytes_written = 0,
+    js_evals = 0,
+    js_filter = 0,
+    js_filter_error = 0,
+    js_filtered_docs = 0,
+    mango_eval_match = 0,
+    %% TODO: switch record definitions to be macro based, eg:
+    %% ?COUCH_BT_GET_KP_NODE = 0,
+    get_kv_node = 0,
+    get_kp_node = 0
+}).
+
+%% monotonic time now in millisecionds
+tnow() ->
+    erlang:monotonic_time(millisecond).
+
+is_enabled() ->
+    config:get_boolean(?MODULE_STRING, "enabled", true).
+
+db_opened() -> inc(db_opened).
+doc_read() -> inc(docs_read).
+row_read() -> inc(rows_read).
+btree_fold() -> inc(?COUCH_BT_FOLDS).
+%% TODO: do we need ioq_called and this access pattern?
+ioq_called() -> is_enabled() andalso inc(ioq_calls).
+js_evaled() -> inc(js_evals).
+js_filtered() -> inc(js_filter).
+js_filtered_error() -> inc(js_filter_error).
+js_filtered_doc() -> inc(js_filtered_docs).
+mango_match_evaled() -> inc(mango_eval_match).
+get_kv_node() -> inc(get_kv_node).
+get_kp_node() -> inc(get_kp_node).
+
+js_filtered_docs(N) -> inc(js_filtered_docs, N).
+io_bytes_read(N) -> inc(io_bytes_read, N).
+io_bytes_written(N) -> inc(io_bytes_written, N).
+
+inc(?DB_OPEN) ->
+    inc(?DB_OPEN, 1);
+inc(docs_read) ->
+    inc(docs_read, 1);
+inc(?ROWS_READ) ->
+    inc(?ROWS_READ, 1);
+inc(?FRPC_CHANGES_RETURNED) ->
+    inc(?FRPC_CHANGES_RETURNED, 1);
+inc(?COUCH_BT_FOLDS) ->
+    inc(?COUCH_BT_FOLDS, 1);
+inc(ioq_calls) ->
+    inc(ioq_calls, 1);
+inc(io_bytes_read) ->
+    inc(io_bytes_read, 1);
+inc(io_bytes_written) ->
+    inc(io_bytes_written, 1);
+inc(js_evals) ->
+    inc(js_evals, 1);
+inc(?COUCH_JS_FILTER) ->
+    inc(?COUCH_JS_FILTER, 1);
+inc(?COUCH_JS_FILTER_ERROR) ->
+    inc(?COUCH_JS_FILTER_ERROR, 1);
+inc(?COUCH_JS_FILTERED_DOCS) ->
+    inc(?COUCH_JS_FILTERED_DOCS, 1);
+inc(?MANGO_EVAL_MATCH) ->
+    inc(?MANGO_EVAL_MATCH, 1);
+inc(?COUCH_BT_GET_KP_NODE) ->
+    inc(?COUCH_BT_GET_KP_NODE, 1);
+inc(?COUCH_BT_GET_KV_NODE) ->
+    inc(?COUCH_BT_GET_KV_NODE, 1);
+inc(_) ->
+    0.
+
+
+inc(?DB_OPEN, N) ->
+    update_counter(#rctx.?DB_OPEN, N);
+inc(?ROWS_READ, N) ->
+    update_counter(#rctx.?ROWS_READ, N);
+inc(?FRPC_CHANGES_RETURNED, N) ->
+    update_counter(#rctx.?FRPC_CHANGES_RETURNED, N);
+inc(ioq_calls, N) ->
+    update_counter(#rctx.ioq_calls, N);
+inc(io_bytes_read, N) ->
+    update_counter(#rctx.io_bytes_read, N);
+inc(io_bytes_written, N) ->
+    update_counter(#rctx.io_bytes_written, N);
+inc(js_evals, N) ->
+    update_counter(#rctx.js_evals, N);
+inc(?COUCH_JS_FILTER, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER, N);
+inc(?COUCH_JS_FILTER_ERROR, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTER_ERROR, N);
+inc(?COUCH_JS_FILTERED_DOCS, N) ->
+    update_counter(#rctx.?COUCH_JS_FILTERED_DOCS, N);
+inc(?MANGO_EVAL_MATCH, N) ->
+    update_counter(#rctx.?MANGO_EVAL_MATCH, N);
+inc(?DB_OPEN_DOC, N) ->
+    update_counter(#rctx.?DB_OPEN_DOC, N);
+inc(?FRPC_CHANGES_ROW, N) ->
+    update_counter(#rctx.?ROWS_READ, N); %% TODO: rework double use of rows_read
+inc(?COUCH_BT_GET_KP_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KP_NODE, N);
+inc(?COUCH_BT_GET_KV_NODE, N) ->
+    update_counter(#rctx.?COUCH_BT_GET_KV_NODE, N);
+inc(_, _) ->
+    %% inc needs to allow unknown types to pass for accumulate_update to handle
+    %% updates from nodes with newer data formats
+    0.
+
+maybe_inc([mango, evaluate_selector], Val) ->
+    inc(?MANGO_EVAL_MATCH, Val);
+maybe_inc([couchdb, database_reads], Val) ->
+    inc(?DB_OPEN_DOC, Val);
+maybe_inc([fabric_rpc, changes, processed], Val) ->
+    inc(?FRPC_CHANGES_ROW, Val);
+maybe_inc([fabric_rpc, changes, returned], Val) ->
+    inc(?FRPC_CHANGES_RETURNED, Val);
+maybe_inc([fabric_rpc, view, rows_read], Val) ->
+    inc(?ROWS_READ, Val);
+maybe_inc([couchdb, couch_server, open], Val) ->
+    inc(?DB_OPEN, Val);
+maybe_inc([couchdb, btree, folds], Val) ->
+    inc(?COUCH_BT_FOLDS, Val);
+maybe_inc([couchdb, btree, kp_node], Val) ->
+    inc(?COUCH_BT_GET_KP_NODE, Val);
+maybe_inc([couchdb, btree, kv_node], Val) ->
+    inc(?COUCH_BT_GET_KV_NODE, Val);
+maybe_inc([couchdb, query_server, js_filter_error], Val) ->
+    inc(?COUCH_JS_FILTER_ERROR, Val);
+maybe_inc([couchdb, query_server, js_filter], Val) ->
+    inc(?COUCH_JS_FILTER, Val);
+maybe_inc([couchdb, query_server, js_filtered_docs], Val) ->
+    inc(?COUCH_JS_FILTERED_DOCS, Val);
+maybe_inc(_Metric, _Val) ->
+    %%io:format("SKIPPING MAYBE_INC METRIC[~p]: ~p~n", [Val, Metric]),
+    0.
+
+
+%% TODO: update stats_descriptions.cfg for relevant apps
+should_track([fabric_rpc, all_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, processed]) ->
+    is_enabled();
+should_track([fabric_rpc, changes, returned]) ->
+    is_enabled();
+should_track([fabric_rpc, map_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, reduce_view, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, get_all_security, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_doc, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, update_docs, spawned]) ->
+    is_enabled();
+should_track([fabric_rpc, open_shard, spawned]) ->
+    is_enabled();
+should_track([mango_cursor, view, all_docs]) ->
+    is_enabled();
+should_track([mango_cursor, view, idx]) ->
+    is_enabled();
+should_track(_Metric) ->
+    %%io:format("SKIPPING METRIC: ~p~n", [Metric]),
+    false.
+
+accumulate_delta(Delta) when is_map(Delta) ->
+    %% TODO: switch to creating a batch of updates to invoke a single
+    %% update_counter rather than sequentially invoking it for each field
+    is_enabled() andalso maps:foreach(fun inc/2, Delta);
+accumulate_delta(undefined) ->
+    ok;
+accumulate_delta(Other) ->
+    io:format("CSRT:ACC_DELTA UNKNOWN DELTA: ~p~n", [Other]).
+
+
+update_counter(Field, Count) ->
+    is_enabled() andalso update_counter(get_pid_ref(), Field, Count).
+
+
+update_counter({_Pid,_Ref}=PidRef, Field, Count) ->
+    %% TODO: mem3 crashes without catch, why do we lose the stats table?
+    is_enabled() andalso catch ets:update_counter(?MODULE, PidRef, {Field, Count}, #rctx{pid_ref=PidRef}).
+
+
+active() -> active_int(all).
+active_coordinators() -> active_int(coordinators).
+active_workers() -> active_int(workers).
+
+
+active_int(coordinators) ->
+    select_by_type(coordinators);
+active_int(workers) ->
+    select_by_type(workers);
+active_int(all) ->
+    lists:map(fun to_flat_json/1, ets:tab2list(?MODULE)).
+
+
+select_by_type(coordinators) ->
+    ets:select(couch_stats_resource_tracker,
+        [{#rctx{type = {coordinator,'_','_'}, _ = '_'}, [], ['$_']}]);
+select_by_type(workers) ->
+    ets:select(couch_stats_resource_tracker,

Review Comment:
   ```erlang
   (node1@127.0.0.1)17> ets:fun2ms(fun(#rctx{type = {worker, _, _}} = R) -> R end).
   [{#rctx{started_at = '_',updated_at = '_',exited_at = '_',
           pid_ref = '_',mon_ref = '_',mfa = '_',nonce = '_',
           from = '_',
           type = {worker,'_','_'},
           state = '_',dbname = '_',username = '_',db_open = '_',
           docs_read = '_',rows_read = '_',btree_folds = '_',
           changes_processed = '_',changes_returned = '_',
           ioq_calls = '_',io_bytes_read = '_',io_bytes_written = '_',
           js_evals = '_',js_filter = '_',js_filter_error = '_',
           js_filtered_docs = '_',mango_eval_match = '_',...},
     [],
     ['$_']}]
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "pgj (via GitHub)" <gi...@apache.org>.
pgj commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1413113094


##########
src/rexi/src/rexi_utils.erl:
##########
@@ -100,6 +110,13 @@ process_message(RefList, Keypos, Fun, Acc0, TimeoutRef, PerMsgTO) ->
             end;
         {rexi_DOWN, _, _, _} = Msg ->
             Fun(Msg, nil, Acc0)
-    after PerMsgTO ->
-        {timeout, Acc0}
     end.
+
+extract_delta({A, {delta, Delta}}) -> {{A}, Delta};

Review Comment:
   Please consider this shorter (and somewhat more flexible) version:
   
   ```erlang
   extract_delta(T) ->
       Size = erlang:size(T),
       case erlang:element(Size, T) of
           {delta, Delta} when 0 < Size, Size < 9 -> {erlang:delete_element(Size, T), Delta};
           _ -> {T, undefined}
       end.
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] PoC Couch Stats Resource Tracker for tracking process local resource usage [couchdb]

Posted by "pgj (via GitHub)" <gi...@apache.org>.
pgj commented on code in PR #4812:
URL: https://github.com/apache/couchdb/pull/4812#discussion_r1413085196


##########
src/rexi/src/rexi.erl:
##########
@@ -328,3 +336,23 @@ drain_acks(Count) ->
     after 0 ->
         {ok, Count}
     end.
+
+get_delta() ->
+    {delta, couch_stats_resource_tracker:make_delta()}.
+
+maybe_add_delta(T) ->
+    case couch_stats_resource_tracker:is_enabled() of
+        false ->
+            T;
+        true ->
+            add_delta(T, get_delta())
+    end.
+
+add_delta({A}, Delta) -> {A, Delta};

Review Comment:
   The same could be expressed with the help of `erlang:insert_element/3` and `erlang:size/1`:
   
   ```erlang
   add_delta(T, Delta) -> erlang:insert_element(erlang:size(T) + 1, T, Delta).
   ```
   
   If maximum tuple size matters (semantically equivalent):
   
   ```erlang
   add_delta(T, Delta) when 0 < erlang:size(T), erlang:size(T) < 8 ->
       erlang:insert_element(erlang:size(T) + 1, T, Delta);
   add_delta(T, _Delta) -> T.
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@couchdb.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org