Add Fase 0 telemetry for the server-client migration baseline

Adds dev-only counters, with no behavior change, to answer the open
questions from the migration plan before touching the transport:

- frontend.state/get-invoke-stats: per-thread-api-method count/total-ms/
  max-ms for every <invoke-db-worker call, to census how many invokes a
  typical navigation generates and their latency distribution.
- db-worker-node: per-repo in-flight /v1/invoke counter that logs a new
  concurrency high-water mark, to check whether concurrent client writes
  actually overlap before deciding on a write queue.
- subs-loader/get-stats: dedup-hit vs new-entry counts for load!, and
  batch-size distribution per slot kind at flush, to check whether
  in-flight snapshot requests are already deduplicated and whether the
  {:children 25 :resources 25} limits are worth raising for network RTT.

Verified with clojure -M:cljs compile app and compile db-worker-node
(0 warnings on both).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:51:09 +02:00
parent 40631362b3
commit 52971f8ec9
3 changed files with 77 additions and 7 deletions

View File

@@ -8,6 +8,22 @@
(defonce ^:private *batch (atom {}))
(defonce ^:private *stats (atom {:dedup-hits 0 :new-entries 0 :flushes 0 :batch-sizes {}}))
(defn get-stats
"Snapshot of subs-loader batching counters: :dedup-hits counts load! calls
that reused an already-pending slot request within the same unflushed
batch window (see `load!`); :batch-sizes tallies entries per slot kind
across flushes. Dev-only instrumentation for the server-client migration
latency baseline (see docs/agent-guide); does not affect batching or the
`limits` above."
[]
@*stats)
(defn reset-stats!
[]
(reset! *stats {:dedup-hits 0 :new-entries 0 :flushes 0 :batch-sizes {}}))
(defn- reject!
[entries error]
(doseq [{:keys [result]} entries]
@@ -45,6 +61,13 @@
[]
(when @state/db-worker-ready?
(let [batch (take-batch!)]
(swap! *stats
(fn [s]
(-> s
(update :flushes inc)
(update :batch-sizes
#(merge-with + %
(frequencies (map (comp slot-kind first :slot-key) batch)))))))
(doseq [[graph-id graph-entries] (group-by :graph-id batch)
entries (request-groups graph-entries)]
(-> (state/<invoke-db-worker :thread-api/get-render-snapshots
@@ -72,9 +95,11 @@
[graph-id slot-key schedule!]
(let [entry-key [graph-id slot-key]]
(if-let [result (get-in @*batch [entry-key :result])]
result
(do (swap! *stats update :dedup-hits inc)
result)
(let [result (p/deferred)
schedule? (empty? @*batch)]
(swap! *stats update :new-entries inc)
(swap! *batch assoc entry-key
{:graph-id graph-id :slot-key slot-key :result result})
(when schedule?

View File

@@ -63,6 +63,28 @@
(reset! ready? (fn? worker))))
ready?))
(defonce ^:private *invoke-stats (atom {}))
(defn get-invoke-stats
"Snapshot of db-worker invoke counters, keyed by thread-api method kw. Each
entry is {:count N :total-ms N :max-ms N}. Dev-only instrumentation for the
server-client migration latency baseline (see docs/agent-guide); does not
affect invocation behavior."
[]
@*invoke-stats)
(defn reset-invoke-stats!
[]
(reset! *invoke-stats {}))
(defn- record-invoke-stat!
[qkw elapsed-ms]
(swap! *invoke-stats update qkw
(fn [{:keys [count total-ms max-ms] :or {count 0 total-ms 0 max-ms 0}}]
{:count (inc count)
:total-ms (+ total-ms elapsed-ms)
:max-ms (max max-ms elapsed-ms)})))
(defn <invoke-db-worker
"invoke db-worker thread api"
[qkw & args]
@@ -70,11 +92,13 @@
(when (nil? worker)
(prn :<invoke-db-worker-error qkw)
(throw (ex-info "db-worker has not been initialized" {})))
(p/let [result (apply worker qkw args)]
(if (or (instance? ExceptionInfo result)
(instance? js/Error result))
(p/rejected result)
result))))
(let [started-at (js/performance.now)]
(-> (p/let [result (apply worker qkw args)]
(if (or (instance? ExceptionInfo result)
(instance? js/Error result))
(p/rejected result)
result))
(p/finally (fn [] (record-invoke-stat! qkw (- (js/performance.now) started-at))))))))
(def ^:private export-block-text-indent-styles #{"dashes" "spaces" "no-indent"})

View File

@@ -20,6 +20,25 @@
(defonce ^:private *sse-clients (atom #{}))
(defonce ^:private *lock-info (atom nil))
(defonce ^:private *server-list-file (atom nil))
(defonce ^:private *inflight-by-repo (atom {}))
(defonce ^:private *max-inflight-by-repo (atom {}))
(defn- invoke-started!
"Increments the /v1/invoke in-flight counter for repo and logs a new
concurrency high-water mark when observed. Dev-only instrumentation for the
server-client migration multi-client baseline (see docs/agent-guide); has no
effect on request handling."
[repo]
(let [n (get (swap! *inflight-by-repo update repo (fnil inc 0)) repo)
prev-max (get @*max-inflight-by-repo repo 0)]
(when (> n prev-max)
(swap! *max-inflight-by-repo assoc repo n)
(log/info :db-worker-node-invoke-concurrency-high-water-mark
{:repo repo :concurrent n}))))
(defn- invoke-finished!
[repo]
(swap! *inflight-by-repo update repo (fnil dec 0)))
(defn- server-list-file-path
[root-dir]
@@ -376,7 +395,9 @@
(p/let [_ (when-not (contains? non-repo-methods method-kw)
(let [{:keys [path lock]} @*lock-info]
(db-lock/assert-lock-owner! path lock)))
result (<invoke! proxy method-str method-kw args')]
_ (invoke-started! bound-repo)
result (-> (<invoke! proxy method-str method-kw args')
(p/finally (fn [] (invoke-finished! bound-repo))))]
(send-json! res 200 {:ok true :resultTransit result}))))
(p/catch (fn [error]
(log-invoke-error! res error method-kw)))))