@@ -78,6 +78,12 @@ type MetricsCollector struct {
7878 maxMemoryBytes int64
7979 currentMemoryBytes int64
8080 privacyLevel int
81+
82+ // Caching for expensive calculations
83+ cacheMutex sync.RWMutex
84+ cachedMetrics map [string ]interface {}
85+ cacheLastUpdate time.Time
86+ cacheTTL time.Duration
8187}
8288
8389// QueryLogEntry - Entry for the query log
@@ -112,6 +118,12 @@ type MonitoringUI struct {
112118 clients map [* websocket.Conn ]bool
113119 clientsMutex sync.Mutex
114120 proxy * Proxy
121+
122+ // WebSocket broadcast rate limiting
123+ broadcastMutex sync.Mutex
124+ lastBroadcast time.Time
125+ broadcastMinDelay time.Duration
126+ pendingBroadcast bool
115127}
116128
117129// NewMonitoringUI - Creates a new monitoring UI
@@ -145,6 +157,9 @@ func NewMonitoringUI(proxy *Proxy) *MonitoringUI {
145157 maxMemoryBytes : int64 (maxMemoryMB * 1024 * 1024 ),
146158 currentMemoryBytes : 0 ,
147159 privacyLevel : proxy .monitoringUI .PrivacyLevel ,
160+ // Initialize caching with 1 second TTL
161+ cacheTTL : time .Second ,
162+ cachedMetrics : make (map [string ]interface {}),
148163 }
149164
150165 dlog .Debugf ("Metrics collector initialized with privacy level: %d" , metricsCollector .privacyLevel )
@@ -173,6 +188,8 @@ func NewMonitoringUI(proxy *Proxy) *MonitoringUI {
173188 },
174189 clients : make (map [* websocket.Conn ]bool ),
175190 proxy : proxy ,
191+ // Initialize broadcast rate limiting with 100ms minimum delay
192+ broadcastMinDelay : 100 * time .Millisecond ,
176193 }
177194}
178195
@@ -265,6 +282,9 @@ func (ui *MonitoringUI) UpdateMetrics(pluginsState PluginsState, msg *dns.Msg, s
265282 }
266283 mc .countersMutex .Unlock ()
267284
285+ // Invalidate cache since counters changed
286+ mc .invalidateCache ()
287+
268288 // Update query types - separate lock
269289 if msg != nil && len (msg .Question ) > 0 {
270290 question := msg .Question [0 ]
@@ -392,14 +412,31 @@ func (ui *MonitoringUI) UpdateMetrics(pluginsState PluginsState, msg *dns.Msg, s
392412 mc .queryLogMutex .Unlock ()
393413 }
394414
395- // Broadcast updates to WebSocket clients
396- go ui .broadcastMetrics ()
415+ // Broadcast updates to WebSocket clients (rate limited)
416+ ui .scheduleBroadcast ()
417+ }
418+
419+ // invalidateCache - Marks the cache as stale (call when data changes)
420+ func (mc * MetricsCollector ) invalidateCache () {
421+ mc .cacheMutex .Lock ()
422+ mc .cacheLastUpdate = time.Time {} // Zero time to force refresh
423+ mc .cacheMutex .Unlock ()
397424}
398425
399426// GetMetrics - Returns the current metrics
400427func (mc * MetricsCollector ) GetMetrics () map [string ]interface {} {
401428 dlog .Debugf ("GetMetrics called" )
402429
430+ // Check cache first
431+ mc .cacheMutex .RLock ()
432+ if time .Since (mc .cacheLastUpdate ) < mc .cacheTTL && mc .cachedMetrics != nil {
433+ cached := mc .cachedMetrics
434+ mc .cacheMutex .RUnlock ()
435+ dlog .Debugf ("Returning cached metrics" )
436+ return cached
437+ }
438+ mc .cacheMutex .RUnlock ()
439+
403440 // Read basic counters first
404441 mc .countersMutex .RLock ()
405442 totalQueries := mc .totalQueries
@@ -551,8 +588,8 @@ func (mc *MetricsCollector) GetMetrics() map[string]interface{} {
551588 copy (recentQueries , mc .recentQueries )
552589 mc .queryLogMutex .RUnlock ()
553590
554- // Return all metrics
555- return map [string ]interface {}{
591+ // Return all metrics and cache the result
592+ metrics := map [string ]interface {}{
556593 "total_queries" : totalQueries ,
557594 "queries_per_second" : queriesPerSecond ,
558595 "uptime_seconds" : time .Since (startTime ).Seconds (),
@@ -566,22 +603,44 @@ func (mc *MetricsCollector) GetMetrics() map[string]interface{} {
566603 "query_types" : queryTypesList ,
567604 "recent_queries" : recentQueries ,
568605 }
606+
607+ // Cache the computed metrics
608+ mc .cacheMutex .Lock ()
609+ mc .cachedMetrics = metrics
610+ mc .cacheLastUpdate = time .Now ()
611+ mc .cacheMutex .Unlock ()
612+
613+ dlog .Debugf ("Computed and cached new metrics" )
614+ return metrics
569615}
570616
571617// setCORSHeaders - Sets standard CORS headers for all responses
572618func setCORSHeaders (w http.ResponseWriter ) {
573619 w .Header ().Set ("Access-Control-Allow-Origin" , "*" )
574620 w .Header ().Set ("Access-Control-Allow-Methods" , "GET, OPTIONS" )
575621 w .Header ().Set ("Access-Control-Allow-Headers" , "Content-Type" )
622+ }
623+
624+ // setDynamicCacheHeaders - Sets cache headers for dynamic content (metrics, API)
625+ func setDynamicCacheHeaders (w http.ResponseWriter ) {
576626 w .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
577627 w .Header ().Set ("Pragma" , "no-cache" )
578628 w .Header ().Set ("Expires" , "0" )
579629}
580630
631+ // setStaticCacheHeaders - Sets cache headers for static content
632+ func setStaticCacheHeaders (w http.ResponseWriter , maxAge int ) {
633+ w .Header ().Set ("Cache-Control" , fmt .Sprintf ("public, max-age=%d" , maxAge ))
634+ w .Header ().Set ("Expires" , time .Now ().Add (time .Duration (maxAge )* time .Second ).Format (http .TimeFormat ))
635+ }
636+
581637// handleTestQuery - Handles test query requests for debugging
582638func (ui * MonitoringUI ) handleTestQuery (w http.ResponseWriter , r * http.Request ) {
583639 dlog .Debugf ("Adding test query" )
584640
641+ // Test queries modify state - no cache
642+ setDynamicCacheHeaders (w )
643+
585644 // Create a fake DNS message
586645 msg := & dns.Msg {}
587646 msg .SetQuestion ("test.example.com." , dns .TypeA )
@@ -629,6 +688,9 @@ func (ui *MonitoringUI) handleRoot(w http.ResponseWriter, r *http.Request) {
629688
630689 // If this is a simple version request, return a simple page
631690 if r .URL .Query ().Get ("simple" ) == "1" {
691+ // Simple page has dynamic content - no cache
692+ setDynamicCacheHeaders (w )
693+
632694 metrics := ui .metricsCollector .GetMetrics ()
633695
634696 // Create a simple HTML page with the metrics
@@ -647,7 +709,8 @@ func (ui *MonitoringUI) handleRoot(w http.ResponseWriter, r *http.Request) {
647709 return
648710 }
649711
650- // Serve the main dashboard page
712+ // Serve the main dashboard page - cache for 5 minutes since template is static
713+ setStaticCacheHeaders (w , 300 )
651714 w .Header ().Set ("Content-Type" , "text/html" )
652715 w .Write ([]byte (MainHTMLTemplate ))
653716}
@@ -656,8 +719,9 @@ func (ui *MonitoringUI) handleRoot(w http.ResponseWriter, r *http.Request) {
656719func (ui * MonitoringUI ) handleMetrics (w http.ResponseWriter , r * http.Request ) {
657720 dlog .Debugf ("Received metrics request from %s" , r .RemoteAddr )
658721
659- // Set CORS headers
722+ // Set CORS headers and dynamic cache headers for API
660723 setCORSHeaders (w )
724+ setDynamicCacheHeaders (w )
661725
662726 // Handle preflight OPTIONS request
663727 if r .Method == "OPTIONS" {
@@ -837,6 +901,8 @@ func (ui *MonitoringUI) handleStatic(w http.ResponseWriter, r *http.Request) {
837901// handleStaticJS - Serves the JavaScript for the monitoring UI
838902func (ui * MonitoringUI ) handleStaticJS (w http.ResponseWriter , r * http.Request ) {
839903 setCORSHeaders (w )
904+ // JavaScript is static - cache for 1 hour
905+ setStaticCacheHeaders (w , 3600 )
840906 w .Header ().Set ("Content-Type" , "application/javascript" )
841907 w .Write ([]byte (MonitoringJSContent ))
842908}
@@ -863,6 +929,40 @@ func (ui *MonitoringUI) basicAuthMiddleware(next http.Handler) http.Handler {
863929 })
864930}
865931
932+ // scheduleBroadcast - Rate-limited scheduling of WebSocket broadcasts
933+ func (ui * MonitoringUI ) scheduleBroadcast () {
934+ ui .broadcastMutex .Lock ()
935+ defer ui .broadcastMutex .Unlock ()
936+
937+ now := time .Now ()
938+ timeSinceLastBroadcast := now .Sub (ui .lastBroadcast )
939+
940+ if timeSinceLastBroadcast >= ui .broadcastMinDelay {
941+ // Enough time has passed, broadcast immediately
942+ ui .lastBroadcast = now
943+ ui .pendingBroadcast = false
944+ go ui .broadcastMetrics ()
945+ } else {
946+ // Too soon, schedule a delayed broadcast if not already pending
947+ if ! ui .pendingBroadcast {
948+ ui .pendingBroadcast = true
949+ delay := ui .broadcastMinDelay - timeSinceLastBroadcast
950+ go func () {
951+ time .Sleep (delay )
952+ ui .broadcastMutex .Lock ()
953+ if ui .pendingBroadcast {
954+ ui .lastBroadcast = time .Now ()
955+ ui .pendingBroadcast = false
956+ ui .broadcastMutex .Unlock ()
957+ ui .broadcastMetrics ()
958+ } else {
959+ ui .broadcastMutex .Unlock ()
960+ }
961+ }()
962+ }
963+ }
964+ }
965+
866966// broadcastMetrics - Broadcasts metrics to all connected WebSocket clients
867967func (ui * MonitoringUI ) broadcastMetrics () {
868968 metrics := ui .metricsCollector .GetMetrics ()
0 commit comments