LCOV - code coverage report
Current view: top level - wirestead/diagnostics - send_accounting.hpp (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 100.0 % 213 213
Test Date: 2026-10-29 11:36:05 Functions: 100.0 % 54 54
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 92.2 % 128 118

             Branch data     Line data    Source code
       1                 :             : /*
       2                 :             :  * Copyright 2025 Jinwoo Sung
       3                 :             :  *
       4                 :             :  * Licensed under the Apache License, Version 2.0 (the "License");
       5                 :             :  * you may not use this file except in compliance with the License.
       6                 :             :  * You may obtain a copy of the License at
       7                 :             :  *
       8                 :             :  *     http://www.apache.org/licenses/LICENSE-2.0
       9                 :             :  *
      10                 :             :  * Unless required by applicable law or agreed to in writing, software
      11                 :             :  * distributed under the License is distributed on an "AS IS" BASIS,
      12                 :             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13                 :             :  * See the License for the specific language governing permissions and
      14                 :             :  * limitations under the License.
      15                 :             :  */
      16                 :             : 
      17                 :             : #pragma once
      18                 :             : 
      19                 :             : #include <algorithm>
      20                 :             : #include <cstddef>
      21                 :             : #include <cstdint>
      22                 :             : #include <memory>
      23                 :             : #include <mutex>
      24                 :             : #include <unordered_map>
      25                 :             : #include <vector>
      26                 :             : 
      27                 :             : #include "wirestead/wrapper/send_accounting.hpp"
      28                 :             : 
      29                 :             : namespace wirestead::diagnostics {
      30                 :             : 
      31                 :             : // Server callers hold their session-map mutex while transferring or reading
      32                 :             : // contributors. Each source is a coherent ledger snapshot.
      33                 :         972 : inline void accumulate_send_accounting(wrapper::SendAccounting& total, const wrapper::SendAccounting& source) {
      34                 :       10692 :   const auto add = [](wrapper::SendRequestTotals& to, const wrapper::SendRequestTotals& from) {
      35                 :       10692 :     to.requests += from.requests;
      36                 :       10692 :     to.bytes += from.bytes;
      37                 :       10692 :   };
      38                 :        3888 :   const auto loss = [&](wrapper::SendLossTotals& to, const wrapper::SendLossTotals& from) {
      39                 :        3888 :     add(to.discarded_before_write, from.discarded_before_write);
      40                 :        3888 :     add(to.aborted_during_write, from.aborted_during_write);
      41                 :        4860 :   };
      42                 :         972 :   add(total.accepted, source.accepted);
      43                 :         972 :   add(total.written, source.written);
      44                 :         972 :   add(total.outstanding, source.outstanding);
      45                 :         972 :   loss(total.explicit_stop, source.explicit_stop);
      46                 :         972 :   loss(total.connection_loss, source.connection_loss);
      47                 :         972 :   loss(total.queue_pressure, source.queue_pressure);
      48                 :         972 :   loss(total.session_expiry, source.session_expiry);
      49                 :         972 :   total.confirmed_written_bytes += source.confirmed_written_bytes;
      50                 :         972 : }
      51                 :             : 
      52                 :             : // The admitting thread and the executor each take the ledger lock per message
      53                 :             : // or batch, for a few dozen nanoseconds. Sleeping on contention costs a futex
      54                 :             : // round trip on both sides, far longer than the wait itself.
      55                 :             : class BriefMutex {
      56                 :             :  public:
      57                 :      753501 :   void lock() {
      58         [ +  + ]:      773654 :     for (int i = 0; i < 100; ++i)
      59         [ +  + ]:      773554 :       if (mutex_.try_lock()) return;
      60                 :         100 :     mutex_.lock();
      61                 :             :   }
      62                 :      753501 :   void unlock() { mutex_.unlock(); }
      63                 :             :   bool try_lock() { return mutex_.try_lock(); }
      64                 :             : 
      65                 :             :  private:
      66                 :             :   std::mutex mutex_;
      67                 :             : };
      68                 :             : 
      69                 :             : // Shared by admission threads and the transport executor. IDs never repeat
      70                 :             : // across reset; old completions cannot change a replacement measurement epoch.
      71                 :             : class SendAccountingLedger {
      72                 :             :  public:
      73                 :             :   using Request = uint64_t;
      74                 :             :   enum class Cause { ExplicitStop, ConnectionLoss, QueuePressure, SessionExpiry };
      75                 :             : 
      76                 :             :   // A contributor belongs to this ledger's measurement epoch. Entries retain
      77                 :             :   // it while I/O is outstanding, even after a virtual session leaves its map.
      78                 :             :   struct Group {
      79                 :             :     wrapper::SendAccounting totals;
      80                 :             :     uint64_t epoch = 0;
      81                 :             :   };
      82                 :             :   using GroupHandle = std::shared_ptr<Group>;
      83                 :             : 
      84                 :      501825 :   Request admit(size_t bytes, GroupHandle group = {}) {
      85                 :      501825 :     std::lock_guard<BriefMutex> lock(mutex_);
      86                 :             :     // Commit the ID only once the entry is stored: a failed insert changes nothing.
      87                 :      501825 :     const auto id = next_id_ + 1;
      88   [ +  +  +  +  :      501825 :     if (group && group->epoch != epoch_) {
                   +  + ]
      89                 :         129 :       group->totals = {};
      90                 :         129 :       group->epoch = epoch_;
      91                 :             :     }
      92   [ +  +  +  + ]:      501825 :     if (count_ == slots_.size()) make_room();
      93                 :      501823 :     auto& entry = slot(count_);
      94                 :      501823 :     entry = Entry{bytes, epoch_, false, true, std::move(group)};
      95                 :      501823 :     ++count_;
      96                 :      501823 :     next_id_ = id;
      97                 :      501823 :     update(entry, [&](auto& totals) {
      98                 :      511416 :       add(totals.accepted, bytes);
      99                 :      511416 :       add(totals.outstanding, bytes);
     100                 :      511416 :     });
     101                 :      501823 :     return id;
     102                 :      501825 :   }
     103                 :             : 
     104                 :             :   // Roll back tracking if posting throws before the API returns acceptance.
     105                 :             :   class Admission {
     106                 :             :    public:
     107                 :      491503 :     Admission(SendAccountingLedger& ledger, size_t bytes, GroupHandle group = {})
     108                 :      491503 :         : ledger_(ledger), request_(ledger.admit(bytes, std::move(group))) {}
     109                 :             :     Admission(const Admission&) = delete;
     110                 :             :     Admission& operator=(const Admission&) = delete;
     111                 :      491503 :     ~Admission() {
     112         [ +  + ]:      491503 :       if (!committed_) ledger_.rollback(request_);
     113                 :      491503 :     }
     114                 :      291357 :     Request request() const { return request_; }
     115                 :      291358 :     void commit() { committed_ = true; }
     116                 :             : 
     117                 :             :    private:
     118                 :             :     SendAccountingLedger& ledger_;
     119                 :             :     Request request_;
     120                 :             :     bool committed_ = false;
     121                 :             :   };
     122                 :             : 
     123                 :             :   // Called immediately before handing a request to local I/O.
     124                 :        4457 :   bool begin(Request id) {
     125                 :        4457 :     std::lock_guard<BriefMutex> lock(mutex_);
     126                 :        8914 :     return begin_locked(id);
     127                 :        4457 :   }
     128                 :             : 
     129                 :             :   // Projections only read the caller-owned batch. They must not re-enter this
     130                 :             :   // ledger. Returns false, activating nothing, if end() already terminated any
     131                 :             :   // request: the caller must not write the batch. This makes the handoff
     132                 :             :   // atomic with stop/loss without the caller's admission lock.
     133                 :             :   template <typename Range, typename RequestOf>
     134                 :        3728 :   bool begin_batch(const Range& batch, RequestOf request_of) {
     135                 :        3728 :     std::lock_guard<BriefMutex> lock(mutex_);
     136         [ +  + ]:       16367 :     for (const auto& item : batch)
     137   [ +  -  +  + ]:       12745 :       if (!find(request_of(item))) return false;
     138   [ +  -  +  + ]:       16259 :     for (const auto& item : batch) begin_locked(request_of(item));
     139                 :        3622 :     return true;
     140                 :        3728 :   }
     141                 :             : 
     142                 :             :   // One composed-operation completion supplies the prefix length for each
     143                 :             :   // request. A fully covered request is written even if a later request fails.
     144                 :        5953 :   void complete(Request id, size_t confirmed_bytes) {
     145                 :        5953 :     std::lock_guard<BriefMutex> lock(mutex_);
     146                 :        5953 :     complete_locked(id, confirmed_bytes);
     147                 :        5953 :   }
     148                 :             : 
     149                 :             :   // Consume the wire prefix even for retired IDs or older measurement epochs.
     150                 :             :   // Release the ledger mutex before the caller handles connection loss/stop.
     151                 :             :   template <typename Range, typename RequestOf, typename SizeOf>
     152                 :        3476 :   void complete_batch(const Range& batch, size_t confirmed_bytes, RequestOf request_of, SizeOf size_of) {
     153                 :        3476 :     std::lock_guard<BriefMutex> lock(mutex_);
     154         [ +  + ]:       16198 :     for (const auto& item : batch) {
     155                 :       12722 :       const auto confirmed = std::min(confirmed_bytes, size_of(item));
     156                 :       12722 :       complete_locked(request_of(item), confirmed);
     157                 :       12722 :       confirmed_bytes -= confirmed;
     158                 :             :     }
     159                 :        3476 :   }
     160                 :             : 
     161                 :           9 :   void discard(Request id, Cause cause) {
     162                 :           9 :     std::lock_guard<BriefMutex> lock(mutex_);
     163                 :           9 :     auto* entry = find(id);
     164         [ +  + ]:           9 :     if (!entry) return;
     165                 :           7 :     terminate(*entry, cause);
     166                 :           7 :     erase(id, *entry);
     167                 :           9 :   }
     168                 :             : 
     169                 :       12543 :   bool contains(Request id) const {
     170                 :       12543 :     std::lock_guard<BriefMutex> lock(mutex_);
     171   [ +  +  +  - ]:       12543 :     if (id < base_) return sparse_.contains(id);
     172   [ +  +  +  + ]:       12274 :     return id - base_ < count_ && slot(id - base_).live;
     173                 :       12543 :   }
     174                 :             : 
     175                 :             :   // Expiry races with handoff under the caller's admission mutex. Active
     176                 :             :   // operations keep their actual outcome; unrelated contributors are untouched.
     177                 :        8285 :   void discard_waiting(const GroupHandle& group, Cause cause) {
     178         [ -  + ]:        8285 :     if (!group) return;
     179                 :        8285 :     std::lock_guard<BriefMutex> lock(mutex_);
     180         [ +  + ]:       16529 :     for (auto it = sparse_.begin(); it != sparse_.end();) {
     181   [ +  +  +  +  :        8244 :       if (it->second.group == group && !it->second.active) {
                   +  + ]
     182                 :           1 :         terminate(it->second, cause);
     183                 :           1 :         it = sparse_.erase(it);
     184                 :             :       } else {
     185                 :        8243 :         ++it;
     186                 :             :       }
     187                 :             :     }
     188         [ +  + ]:       16698 :     for (size_t i = 0; i < count_; ++i) {
     189                 :        8413 :       auto& entry = slot(i);
     190   [ +  +  +  +  :        8413 :       if (entry.live && entry.group == group && !entry.active) {
             +  +  +  + ]
     191                 :        8274 :         terminate(entry, cause);
     192                 :        8274 :         entry.live = false;
     193                 :        8274 :         entry.group.reset();
     194                 :             :       }
     195                 :             :     }
     196                 :        8285 :     compact();
     197                 :        8285 :   }
     198                 :             : 
     199                 :             :   // The caller serializes this boundary with admission. Clearing every entry
     200                 :             :   // includes requests accepted before their enqueue handler has executed.
     201                 :        5626 :   void end(Cause cause) {
     202                 :        5626 :     std::lock_guard<BriefMutex> lock(mutex_);
     203   [ +  -  +  + ]:        5628 :     for (const auto& [id, entry] : sparse_) terminate(entry, cause);
     204                 :        5626 :     sparse_.clear();
     205         [ +  + ]:      282131 :     for (size_t i = 0; i < count_; ++i) {
     206   [ +  +  +  - ]:      276505 :       if (slot(i).live) terminate(slot(i), cause);
     207                 :      276505 :       slot(i) = Entry{};
     208                 :             :     }
     209                 :        5626 :     head_ = 0;
     210                 :        5626 :     count_ = 0;
     211                 :        5626 :     base_ = next_id_ + 1;
     212                 :        5626 :   }
     213                 :             : 
     214                 :        6828 :   wrapper::SendAccounting snapshot() const {
     215                 :        6828 :     std::lock_guard<BriefMutex> lock(mutex_);
     216                 :        6828 :     return totals_;
     217                 :        6828 :   }
     218                 :             : 
     219                 :         295 :   wrapper::SendAccounting snapshot(const GroupHandle& group) const {
     220                 :         295 :     std::lock_guard<BriefMutex> lock(mutex_);
     221   [ +  -  +  + ]:         590 :     return group && group->epoch == epoch_ ? group->totals : wrapper::SendAccounting{};
     222                 :         295 :   }
     223                 :             : 
     224                 :             :   // A measurement epoch includes only requests accepted since reset. Retained
     225                 :             :   // old requests still transmit but their later completions/cleanup are ignored.
     226                 :         331 :   void reset() {
     227                 :         331 :     std::lock_guard<BriefMutex> lock(mutex_);
     228                 :         331 :     ++epoch_;
     229                 :         331 :     totals_ = {};
     230                 :         331 :   }
     231                 :             : 
     232                 :             :  private:
     233                 :             :   struct Entry {
     234                 :             :     size_t bytes = 0;
     235                 :             :     uint64_t epoch = 0;
     236                 :             :     bool active = false;
     237                 :             :     bool live = false;
     238                 :             :     GroupHandle group;
     239                 :             :   };
     240                 :             : 
     241                 :     2922356 :   Entry& slot(size_t index) { return slots_[(head_ + index) & (slots_.size() - 1)]; }
     242                 :       12272 :   const Entry& slot(size_t index) const { return slots_[(head_ + index) & (slots_.size() - 1)]; }
     243                 :             : 
     244                 :             :   // Grow only when more than half the slots are live. Otherwise detach the
     245                 :             :   // surviving older IDs before reusing the ring, so repeated rollback/expiry
     246                 :             :   // cannot grow storage behind a stalled request. Stage all allocations first:
     247                 :             :   // a failed promotion leaves both stores and every request unchanged.
     248                 :        2184 :   void make_room() {
     249         [ +  + ]:        2184 :     if (!slots_.empty()) {
     250                 :         117 :       size_t live = 0;
     251         [ +  + ]:      547701 :       for (size_t i = 0; i < count_; ++i) live += slot(i).live;
     252         [ +  + ]:         117 :       if (live <= count_ / 2) {
     253                 :           7 :         auto staged = sparse_;
     254                 :           6 :         staged.reserve(staged.size() + live);
     255         [ +  + ]:         102 :         for (size_t i = 0; i < count_; ++i)
     256   [ +  +  +  - ]:          96 :           if (slot(i).live) staged.emplace(base_ + i, slot(i));
     257                 :           6 :         sparse_.swap(staged);
     258         [ +  + ]:         102 :         for (size_t i = 0; i < count_; ++i) slot(i) = Entry{};
     259                 :           6 :         head_ = 0;
     260                 :           6 :         count_ = 0;
     261                 :           6 :         base_ = next_id_ + 1;
     262                 :           6 :         return;
     263                 :           6 :       }
     264                 :             :     }
     265   [ +  +  +  + ]:        2178 :     std::vector<Entry> larger(slots_.empty() ? 16 : slots_.size() * 2);
     266         [ +  + ]:      549632 :     for (size_t i = 0; i < count_; ++i) larger[i] = std::move(slot(i));
     267                 :        2176 :     slots_ = std::move(larger);
     268                 :        2176 :     head_ = 0;
     269                 :        2176 :   }
     270                 :             : 
     271                 :      248668 :   Entry* find(Request id) {
     272         [ +  + ]:      248668 :     if (id < base_) {
     273                 :         900 :       const auto it = sparse_.find(id);
     274         [ +  + ]:         900 :       return it == sparse_.end() ? nullptr : &it->second;
     275                 :             :     }
     276         [ -  + ]:      247768 :     if (id - base_ >= count_) return nullptr;
     277                 :      247768 :     auto& entry = slot(id - base_);
     278         [ +  + ]:      247768 :     return entry.live ? &entry : nullptr;
     279                 :             :   }
     280                 :             : 
     281                 :      218045 :   void erase(Request id, Entry& entry) {
     282         [ +  + ]:      218045 :     if (id < base_) {
     283                 :           6 :       sparse_.erase(id);
     284                 :           6 :       return;
     285                 :             :     }
     286                 :      218039 :     entry.live = false;
     287                 :      218039 :     entry.group.reset();
     288                 :      218039 :     compact();
     289                 :             :   }
     290                 :             : 
     291                 :      226324 :   void compact() {
     292   [ +  +  +  +  :      451546 :     while (count_ && !slot(0).live) {
                   +  + ]
     293                 :      225222 :       head_ = (head_ + 1) & (slots_.size() - 1);
     294                 :      225222 :       --count_;
     295                 :      225222 :       ++base_;
     296                 :             :     }
     297                 :      226324 :   }
     298                 :             : 
     299                 :       17094 :   bool begin_locked(Request id) {
     300                 :       17094 :     auto* entry = find(id);
     301         [ +  + ]:       17094 :     if (!entry) return false;
     302                 :       17093 :     entry->active = true;
     303                 :       17093 :     return true;
     304                 :             :   }
     305                 :             : 
     306                 :       18675 :   void complete_locked(Request id, size_t confirmed_bytes) {
     307                 :       18675 :     auto* found = find(id);
     308         [ +  + ]:       18675 :     if (!found) return;
     309                 :       17893 :     const auto& entry = *found;
     310                 :       17893 :     update(entry, [&](auto& totals) {
     311                 :       17903 :       const auto confirmed = std::min(confirmed_bytes, entry.bytes);
     312                 :       17903 :       totals.confirmed_written_bytes += confirmed;
     313                 :       17903 :       remove(totals.outstanding, entry.bytes);
     314         [ +  + ]:       17903 :       if (confirmed == entry.bytes) {
     315                 :       17606 :         add(totals.written, entry.bytes);
     316                 :             :       } else {
     317                 :         297 :         add(totals.connection_loss.aborted_during_write, entry.bytes);
     318                 :             :       }
     319                 :       17903 :     });
     320                 :       17893 :     erase(id, *found);
     321                 :             :   }
     322                 :             : 
     323                 :      200145 :   void rollback(Request id) {
     324                 :      200145 :     std::lock_guard<BriefMutex> lock(mutex_);
     325                 :      200145 :     auto* entry = find(id);
     326         [ -  + ]:      200145 :     if (!entry) return;
     327                 :      200145 :     update(*entry, [&](auto& totals) {
     328                 :      200146 :       remove(totals.accepted, entry->bytes);
     329                 :      200146 :       remove(totals.outstanding, entry->bytes);
     330                 :      200146 :     });
     331                 :      200145 :     erase(id, *entry);
     332                 :      200145 :   }
     333                 :             :   template <typename F>
     334                 :     1003646 :   void update(const Entry& entry, F&& apply) {
     335         [ +  + ]:     1003646 :     if (entry.epoch != epoch_) return;
     336                 :     1003460 :     apply(totals_);
     337         [ +  + ]:     1003460 :     if (entry.group) apply(entry.group->totals);
     338                 :             :   }
     339                 :     1333909 :   static void add(wrapper::SendRequestTotals& totals, size_t bytes) {
     340                 :     1333909 :     ++totals.requests;
     341                 :     1333909 :     totals.bytes += bytes;
     342                 :     1333909 :   }
     343                 :      711369 :   static void remove(wrapper::SendRequestTotals& totals, size_t bytes) {
     344                 :      711369 :     --totals.requests;
     345                 :      711369 :     totals.bytes -= bytes;
     346                 :      711369 :   }
     347                 :      283785 :   void terminate(const Entry& entry, Cause cause) {
     348                 :      283785 :     update(entry, [&](auto& totals) {
     349         [ +  + ]:      310402 :       auto& loss = cause == Cause::ExplicitStop     ? totals.explicit_stop
     350         [ +  + ]:       17228 :                    : cause == Cause::ConnectionLoss ? totals.connection_loss
     351         [ +  + ]:       16556 :                    : cause == Cause::SessionExpiry  ? totals.session_expiry
     352                 :             :                                                     : totals.queue_pressure;
     353         [ +  + ]:      293174 :       add(entry.active ? loss.aborted_during_write : loss.discarded_before_write, entry.bytes);
     354                 :      293174 :       remove(totals.outstanding, entry.bytes);
     355                 :      293174 :     });
     356                 :      283785 :   }
     357                 :             : 
     358                 :             :   mutable BriefMutex mutex_;
     359                 :             :   // slot(i) is request base_ + i in a power-of-two ring. IDs are assigned in
     360                 :             :   // order and transports retire them nearly in order, so lookup needs no
     361                 :             :   // hashing and steady traffic reuses slots without allocating. Retired holes
     362                 :             :   // trigger sparse promotion before growing a mostly empty ring. Ring capacity
     363                 :             :   // is bounded by max(16, 4 * peak live requests), independent of retired IDs.
     364                 :             :   std::vector<Entry> slots_;
     365                 :             :   // Only detached live IDs precede base_; FIFO traffic never inserts here.
     366                 :             :   std::unordered_map<Request, Entry> sparse_;
     367                 :             :   size_t head_ = 0;
     368                 :             :   size_t count_ = 0;
     369                 :             :   Request base_ = 1;
     370                 :             :   Request next_id_ = 0;
     371                 :             :   uint64_t epoch_ = 0;
     372                 :             :   wrapper::SendAccounting totals_;
     373                 :             : };
     374                 :             : 
     375                 :             : }  // namespace wirestead::diagnostics
        

Generated by: LCOV version 2.0-1