perf(network-guard): peer table as a hash keyed by peer (P2)

ober

6b7c0a9abf6172eaa01414cec5a14957a2a09ddf

diff --git a/jsecmon/network-guard.ss b/jsecmon/network-guard.ss
index 9fd1e83..36f17fd 100644
--- a/jsecmon/network-guard.ss
+++ b/jsecmon/network-guard.ss
@@ -93,7 +93,7 @@
   (def (make-network-guard policy . maybe-now)
     (unless (network-policy? policy)
       (error 'make-network-guard "expected network policy" policy))
-    (make-network-guard-raw policy (make-mutex) '() 0 0 0
+    (make-network-guard-raw policy (make-mutex) (make-hash-table) 0 0 0
                             (if (pair? maybe-now) (car maybe-now) wall-ms)))
 
   (def (with-guard-lock guard thunk)
@@ -102,17 +102,18 @@
       thunk
       (lambda () (mutex-release (network-guard-mutex guard)))))
 
-  (def (find-peer-row rows peer)
-    (let loop ((xs rows))
-      (cond ((null? xs) #f)
-            ((string=? (vector-ref (car xs) 0) peer) (car xs))
-            (else (loop (cdr xs))))))
-
-  (def (prune-peer-rows rows now window-ms)
-    (filter (lambda (row)
-              (or (> (vector-ref row 1) 0)
-                  (< (- now (vector-ref row 2)) window-ms)))
-            rows))
+  ;; Drop inactive rows whose rate window has expired. `sources` is a hash table
+  ;; keyed by peer, so per-peer lookup is O(1); pruning collects the stale keys
+  ;; first so we never remove during iteration.
+  (def (prune-peer-rows! sources now window-ms)
+    (let ((stale '()))
+      (hash-for-each
+       (lambda (peer row)
+         (unless (or (> (vector-ref row 1) 0)
+                     (< (- now (vector-ref row 2)) window-ms))
+           (set! stale (cons peer stale))))
+       sources)
+      (for-each (lambda (peer) (hash-remove! sources peer)) stale)))
 
   (def (network-guard-admit! guard peer)
     (unless (and (string? peer) (not (string=? peer "")))
@@ -122,18 +123,20 @@
      (lambda ()
        (let* ((policy (network-guard-policy guard))
               (now ((network-guard-now guard)))
-              (window (network-policy-rate-window-ms policy)))
+              (window (network-policy-rate-window-ms policy))
+              (sources (network-guard-sources guard)))
          (when (or (= (network-guard-global-window-start guard) 0)
                    (>= (- now (network-guard-global-window-start guard)) window))
            (network-guard-global-window-start-set! guard now)
            (network-guard-global-attempts-set! guard 0))
-         (let* ((rows (prune-peer-rows (network-guard-sources guard) now window))
-                (row (find-peer-row rows peer)))
-           (network-guard-sources-set! guard rows)
-           (when (and (not row) (< (length rows) *max-peer-rows*))
+         (let ((row (hash-get sources peer)))
+           ;; New peer: prune stale rows only when the table is at capacity, then
+           ;; add if there is room. Existing peers skip the O(n) prune entirely.
+           (when (and (not row) (>= (hash-length sources) *max-peer-rows*))
+             (prune-peer-rows! sources now window))
+           (when (and (not row) (< (hash-length sources) *max-peer-rows*))
              (set! row (vector peer 0 now 0))
-             (network-guard-sources-set! guard
-               (cons row (network-guard-sources guard))))
+             (hash-put! sources peer row))
            (network-guard-global-attempts-set!
             guard (+ 1 (network-guard-global-attempts guard)))
            (if (not row)
@@ -162,7 +165,7 @@
     (with-guard-lock
      guard
      (lambda ()
-       (let ((row (find-peer-row (network-guard-sources guard) peer)))
+        (let ((row (hash-get (network-guard-sources guard) peer)))
          (when (and row (> (vector-ref row 1) 0))
            (vector-set! row 1 (- (vector-ref row 1) 1))
            (network-guard-global-active-set!
@@ -175,7 +178,7 @@
     (with-guard-lock
      guard
      (lambda ()
-       (let ((row (find-peer-row (network-guard-sources guard) peer)))
+        (let ((row (hash-get (network-guard-sources guard) peer)))
          (if row (vector-ref row 1) 0)))))
 
   ;; The pool owns exactly worker-count threads and a bounded FIFO.  Server