package main
import (
"context"
"fmt"
"html/template"
"io"
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/fiatjaf/eventstore"
"github.com/fiatjaf/eventstore/badger"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
"github.com/joho/godotenv"
"github.com/nbd-wtf/go-nostr"
)
var (
version string
)
type Config struct {
RelayName string
RelayPubkey string
RelayDescription string
DBPath string
RelayURL string
IndexPath string
StaticPath string
RefreshInterval int
MinimumFollowers int
ArchivalSync bool
RelayContact string
RelayIcon string
MaxAgeDays int
ArchiveReactions bool
IgnoredPubkeys []string
MaxTrustNetwork int
MaxRelays int
MaxOneHopNetwork int
}
var pool *nostr.SimplePool
var wdb nostr.RelayStore
var relays []string
var relaySet = make(map[string]bool) // O(1) lookup
var config Config
var trustNetwork []string
var trustNetworkSet = make(map[string]bool) // O(1) lookup
var seedRelays []string
var booted bool
var oneHopNetwork []string
var oneHopNetworkSet = make(map[string]bool) // O(1) lookup
var trustNetworkMap map[string]bool
var pubkeyFollowerCount = make(map[string]int)
var trustedNotes uint64
var untrustedNotes uint64
var archiveEventSemaphore = make(chan struct{}, 20) // Reduced from 100 to 20
// Performance counters
var (
totalEvents uint64
rejectedEvents uint64
archivedEvents uint64
profileRefreshCount uint64
networkRefreshCount uint64
)
// Mutexes for thread safety
var (
relayMutex sync.RWMutex
trustNetworkMutex sync.RWMutex
oneHopMutex sync.RWMutex
followerMutex sync.RWMutex
)
func main() {
nostr.InfoLogger = log.New(io.Discard, "", 0)
booted = false
green := "\033[32m"
reset := "\033[0m"
art := `
888 888 88888888888 8888888b. 888
888 o 888 888 888 Y88b 888
888 d8b 888 888 888 888 888
888 d888b 888 .d88b. 888 888 d88P .d88b. 888 8888b. 888 888
888d88888b888 d88""88b 888 8888888P" d8P Y8b 888 "88b 888 888
88888P Y88888 888 888 888 888 T88b 88888888 888 .d888888 888 888
8888P Y8888 Y88..88P 888 888 T88b Y8b. 888 888 888 Y88b 888
888P Y888 "Y88P" 888 888 T88b "Y8888 888 "Y888888 "Y88888
888
Y8b d88P
powered by: khatru "Y88P"
`
fmt.Println(green + art + reset)
log.Println("๐ booting up web of trust relay")
relay := khatru.NewRelay()
ctx := context.Background()
pool = nostr.NewSimplePool(ctx)
config = LoadConfig()
relay.Info.Name = config.RelayName
relay.Info.PubKey = config.RelayPubkey
relay.Info.Icon = config.RelayIcon
relay.Info.Contact = config.RelayContact
relay.Info.Description = config.RelayDescription
relay.Info.Software = "https://github.com/bitvora/wot-relay"
relay.Info.Version = version
appendPubkey(config.RelayPubkey)
db := getDB()
if err := db.Init(); err != nil {
panic(err)
}
wdb = eventstore.RelayWrapper{Store: &db}
relay.RejectEvent = append(relay.RejectEvent,
policies.RejectEventsWithBase64Media,
policies.EventIPRateLimiter(5, time.Minute*1, 30),
)
relay.RejectFilter = append(relay.RejectFilter,
policies.NoEmptyFilters,
policies.NoComplexFilters,
)
relay.RejectConnection = append(relay.RejectConnection,
policies.ConnectionRateLimiter(10, time.Minute*2, 30),
)
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
atomic.AddUint64(&totalEvents, 1)
// Don't reject events if we haven't booted yet or if trust network is empty
if !booted {
return false, ""
}
trustNetworkMutex.RLock()
trusted := trustNetworkMap[event.PubKey]
hasNetwork := len(trustNetworkMap) > 0
trustNetworkMutex.RUnlock()
// If we don't have a trust network yet, allow all events
if !hasNetwork {
return false, ""
}
if !trusted {
atomic.AddUint64(&rejectedEvents, 1)
return true, "not in web of trust"
}
if event.Kind == nostr.KindEncryptedDirectMessage {
atomic.AddUint64(&rejectedEvents, 1)
return true, "only gift wrapped DMs are allowed"
}
return false, ""
})
seedRelays = []string{
"wss://nos.lol",
"wss://nostr.mom",
"wss://purplepag.es",
"wss://purplerelay.com",
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://relay.snort.social",
"wss://relayable.org",
"wss://relay.primal.net",
"wss://relay.nostr.bg",
"wss://no.str.cr",
"wss://nostr21.com",
"wss://nostrue.com",
"wss://relay.siamstr.com",
}
go refreshTrustNetwork(ctx, relay)
go monitorMemoryUsage() // Add memory monitoring
go monitorPerformance() // Add performance monitoring
mux := relay.Router()
static := http.FileServer(http.Dir(config.StaticPath))
mux.Handle("GET /static/", http.StripPrefix("/static/", static))
mux.Handle("GET /favicon.ico", http.StripPrefix("/", static))
// Add debug endpoints
mux.HandleFunc("GET /debug/stats", debugStatsHandler)
mux.HandleFunc("GET /debug/goroutines", debugGoroutinesHandler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles(os.Getenv("INDEX_PATH")))
data := struct {
RelayName string
RelayPubkey string
RelayDescription string
RelayURL string
}{
RelayName: config.RelayName,
RelayPubkey: config.RelayPubkey,
RelayDescription: config.RelayDescription,
RelayURL: config.RelayURL,
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
log.Println("๐ relay running on port :3334")
log.Println("๐ debug endpoints available at:")
log.Println(" http://localhost:3334/debug/pprof/ (CPU/memory profiling)")
log.Println(" http://localhost:3334/debug/stats (application stats)")
log.Println(" http://localhost:3334/debug/goroutines (goroutine info)")
err := http.ListenAndServe(":3334", relay)
if err != nil {
log.Fatal(err)
}
}
func LoadConfig() Config {
godotenv.Load(".env")
if os.Getenv("REFRESH_INTERVAL_HOURS") == "" {
os.Setenv("REFRESH_INTERVAL_HOURS", "3")
}
refreshInterval, _ := strconv.Atoi(os.Getenv("REFRESH_INTERVAL_HOURS"))
log.Println("๐ refresh interval set to", refreshInterval, "hours")
if os.Getenv("MINIMUM_FOLLOWERS") == "" {
os.Setenv("MINIMUM_FOLLOWERS", "1")
}
if os.Getenv("ARCHIVAL_SYNC") == "" {
os.Setenv("ARCHIVAL_SYNC", "TRUE")
}
if os.Getenv("RELAY_ICON") == "" {
os.Setenv("RELAY_ICON", "https://pfp.nostr.build/56306a93a88d4c657d8a3dfa57b55a4ed65b709eee927b5dafaab4d5330db21f.png")
}
if os.Getenv("RELAY_CONTACT") == "" {
os.Setenv("RELAY_CONTACT", getEnv("RELAY_PUBKEY"))
}
if os.Getenv("MAX_AGE_DAYS") == "" {
os.Setenv("MAX_AGE_DAYS", "0")
}
if os.Getenv("ARCHIVE_REACTIONS") == "" {
os.Setenv("ARCHIVE_REACTIONS", "FALSE")
}
if os.Getenv("MAX_TRUST_NETWORK") == "" {
os.Setenv("MAX_TRUST_NETWORK", "40000")
}
if os.Getenv("MAX_RELAYS") == "" {
os.Setenv("MAX_RELAYS", "1000")
}
if os.Getenv("MAX_ONE_HOP_NETWORK") == "" {
os.Setenv("MAX_ONE_HOP_NETWORK", "50000")
}
ignoredPubkeys := []string{}
if ignoreList := os.Getenv("IGNORE_FOLLOWS_LIST"); ignoreList != "" {
ignoredPubkeys = splitAndTrim(ignoreList)
}
minimumFollowers, _ := strconv.Atoi(os.Getenv("MINIMUM_FOLLOWERS"))
maxAgeDays, _ := strconv.Atoi(os.Getenv("MAX_AGE_DAYS"))
maxTrustNetwork, _ := strconv.Atoi(os.Getenv("MAX_TRUST_NETWORK"))
maxRelays, _ := strconv.Atoi(os.Getenv("MAX_RELAYS"))
maxOneHopNetwork, _ := strconv.Atoi(os.Getenv("MAX_ONE_HOP_NETWORK"))
config := Config{
RelayName: getEnv("RELAY_NAME"),
RelayPubkey: getEnv("RELAY_PUBKEY"),
RelayDescription: getEnv("RELAY_DESCRIPTION"),
RelayContact: getEnv("RELAY_CONTACT"),
RelayIcon: getEnv("RELAY_ICON"),
DBPath: getEnv("DB_PATH"),
RelayURL: getEnv("RELAY_URL"),
IndexPath: getEnv("INDEX_PATH"),
StaticPath: getEnv("STATIC_PATH"),
RefreshInterval: refreshInterval,
MinimumFollowers: minimumFollowers,
ArchivalSync: getEnv("ARCHIVAL_SYNC") == "TRUE",
MaxAgeDays: maxAgeDays,
ArchiveReactions: getEnv("ARCHIVE_REACTIONS") == "TRUE",
IgnoredPubkeys: ignoredPubkeys,
MaxTrustNetwork: maxTrustNetwork,
MaxRelays: maxRelays,
MaxOneHopNetwork: maxOneHopNetwork,
}
return config
}
func getEnv(key string) string {
value, exists := os.LookupEnv(key)
if !exists {
log.Fatalf("Environment variable %s not set", key)
}
return value
}
func updateTrustNetworkFilter() {
// Build new trust network in temporary variables
newTrustNetworkMap := make(map[string]bool)
var newTrustNetwork []string
newTrustNetworkSet := make(map[string]bool)
log.Println("๐ building new trust network map")
followerMutex.RLock()
for pubkey, count := range pubkeyFollowerCount {
if count >= config.MinimumFollowers {
newTrustNetworkMap[pubkey] = true
if !newTrustNetworkSet[pubkey] && len(pubkey) == 64 && len(newTrustNetwork) < config.MaxTrustNetwork {
newTrustNetwork = append(newTrustNetwork, pubkey)
newTrustNetworkSet[pubkey] = true
}
}
}
followerMutex.RUnlock()
// Now atomically replace the active trust network
trustNetworkMutex.Lock()
trustNetworkMap = newTrustNetworkMap
trustNetwork = newTrustNetwork
trustNetworkSet = newTrustNetworkSet
trustNetworkMutex.Unlock()
log.Println("๐ trust network map updated with", len(newTrustNetwork), "keys")
// Cleanup follower count map periodically to prevent unbounded growth
followerMutex.Lock()
if len(pubkeyFollowerCount) > config.MaxOneHopNetwork*2 {
log.Println("๐งน cleaning follower count map")
newFollowerCount := make(map[string]int)
for pubkey, count := range pubkeyFollowerCount {
if count >= config.MinimumFollowers || newTrustNetworkMap[pubkey] {
newFollowerCount[pubkey] = count
}
}
oldCount := len(pubkeyFollowerCount)
pubkeyFollowerCount = newFollowerCount
log.Printf("๐งน cleaned follower count map: %d -> %d entries", oldCount, len(newFollowerCount))
}
followerMutex.Unlock()
}
func refreshProfiles(ctx context.Context) {
atomic.AddUint64(&profileRefreshCount, 1)
start := time.Now()
// Get a snapshot of current trust network to avoid holding locks during network operations
trustNetworkMutex.RLock()
currentTrustNetwork := make([]string, len(trustNetwork))
copy(currentTrustNetwork, trustNetwork)
trustNetworkMutex.RUnlock()
for i := 0; i < len(currentTrustNetwork); i += 200 {
timeout, cancel := context.WithTimeout(ctx, 4*time.Second)
end := i + 200
if end > len(currentTrustNetwork) {
end = len(currentTrustNetwork)
}
filters := []nostr.Filter{{
Authors: currentTrustNetwork[i:end],
Kinds: []int{nostr.KindProfileMetadata},
}}
for ev := range pool.SubManyEose(timeout, seedRelays, filters) {
wdb.Publish(ctx, *ev.Event)
}
cancel() // Cancel after each iteration
}
duration := time.Since(start)
log.Printf("๐ค profiles refreshed: %d profiles in %v", len(currentTrustNetwork), duration)
}
func refreshTrustNetwork(ctx context.Context, relay *khatru.Relay) {
runTrustNetworkRefresh := func() {
atomic.AddUint64(&networkRefreshCount, 1)
start := time.Now()
// Build new networks in temporary variables to avoid disrupting the active network
var newOneHopNetwork []string
newOneHopNetworkSet := make(map[string]bool)
newPubkeyFollowerCount := make(map[string]int)
// Copy existing follower counts to preserve data
followerMutex.RLock()
for k, v := range pubkeyFollowerCount {
newPubkeyFollowerCount[k] = v
}
followerMutex.RUnlock()
timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
filters := []nostr.Filter{{
Authors: []string{config.RelayPubkey},
Kinds: []int{nostr.KindFollowList},
}}
log.Println("๐ fetching owner's follows")
eventCount := 0
for ev := range pool.SubManyEose(timeoutCtx, seedRelays, filters) {
eventCount++
for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) {
pubkey := contact[1]
if isIgnored(pubkey, config.IgnoredPubkeys) {
fmt.Println("ignoring follows from pubkey: ", pubkey)
continue
}
newPubkeyFollowerCount[contact[1]]++
// Add to new one-hop network
if !newOneHopNetworkSet[contact[1]] && len(contact[1]) == 64 && len(newOneHopNetwork) < config.MaxOneHopNetwork {
newOneHopNetwork = append(newOneHopNetwork, contact[1])
newOneHopNetworkSet[contact[1]] = true
}
}
}
log.Printf("๐ processed %d follow list events", eventCount)
log.Println("๐ building web of trust graph")
totalProcessed := 0
for i := 0; i < len(newOneHopNetwork); i += 100 {
timeout, cancel := context.WithTimeout(ctx, 4*time.Second)
end := i + 100
if end > len(newOneHopNetwork) {
end = len(newOneHopNetwork)
}
filters = []nostr.Filter{{
Authors: newOneHopNetwork[i:end],
Kinds: []int{nostr.KindFollowList, nostr.KindRelayListMetadata, nostr.KindProfileMetadata},
}}
batchCount := 0
for ev := range pool.SubManyEose(timeout, seedRelays, filters) {
batchCount++
totalProcessed++
for _, contact := range ev.Event.Tags.GetAll([]string{"p"}) {
if len(contact) > 1 {
newPubkeyFollowerCount[contact[1]]++
}
}
for _, relay := range ev.Event.Tags.GetAll([]string{"r"}) {
appendRelay(relay[1])
}
if ev.Event.Kind == nostr.KindProfileMetadata {
wdb.Publish(ctx, *ev.Event)
}
}
cancel() // Cancel after each iteration
if i%500 == 0 { // Log progress every 5 batches
log.Printf("๐ processed batch %d-%d (%d events in this batch)", i, end, batchCount)
}
}
// Now atomically replace the active data structures
oneHopMutex.Lock()
oneHopNetwork = newOneHopNetwork
oneHopNetworkSet = newOneHopNetworkSet
oneHopMutex.Unlock()
followerMutex.Lock()
pubkeyFollowerCount = newPubkeyFollowerCount
followerMutex.Unlock()
duration := time.Since(start)
log.Printf("๐ซ total network size: %d (processed %d events in %v)", len(newPubkeyFollowerCount), totalProcessed, duration)
relayMutex.RLock()
log.Println("๐ relays discovered:", len(relays))
relayMutex.RUnlock()
}
ticker := time.NewTicker(time.Duration(config.RefreshInterval) * time.Hour)
defer ticker.Stop()
// Run initial refresh
log.Println("๐ performing initial trust network build...")
runTrustNetworkRefresh()
updateTrustNetworkFilter()
// Mark as booted after initial trust network is built
booted = true
log.Println("โ
trust network initialized, relay is now active")
deleteOldNotes(relay)
archiveTrustedNotes(ctx, relay)
// Then run on timer
for {
select {
case <-ticker.C:
log.Println("๐ refreshing trust network in background...")
runTrustNetworkRefresh()
updateTrustNetworkFilter()
deleteOldNotes(relay)
archiveTrustedNotes(ctx, relay)
log.Println("โ
trust network refresh completed")
case <-ctx.Done():
return
}
}
}
func appendRelay(relay string) {
relayMutex.Lock()
defer relayMutex.Unlock()
if len(relays) >= config.MaxRelays {
return // Prevent unbounded growth
}
if relaySet[relay] {
return // Already exists
}
relays = append(relays, relay)
relaySet[relay] = true
}
func appendPubkey(pubkey string) {
trustNetworkMutex.Lock()
defer trustNetworkMutex.Unlock()
if len(trustNetwork) >= config.MaxTrustNetwork {
return // Prevent unbounded growth
}
if trustNetworkSet[pubkey] {
return // Already exists
}
if len(pubkey) != 64 {
return
}
trustNetwork = append(trustNetwork, pubkey)
trustNetworkSet[pubkey] = true
}
func archiveTrustedNotes(ctx context.Context, relay *khatru.Relay) {
timeout, cancel := context.WithTimeout(ctx, time.Duration(config.RefreshInterval)*time.Hour)
defer cancel()
done := make(chan struct{})
go func() {
defer close(done)
if config.ArchivalSync {
go refreshProfiles(ctx)
var filters []nostr.Filter
since := nostr.Now()
if config.ArchiveReactions {
filters = []nostr.Filter{{
Kinds: []int{
nostr.KindArticle,
nostr.KindDeletion,
nostr.KindFollowList,
nostr.KindEncryptedDirectMessage,
nostr.KindMuteList,
nostr.KindReaction,
nostr.KindRelayListMetadata,
nostr.KindRepost,
nostr.KindZapRequest,
nostr.KindZap,
nostr.KindTextNote,
},
Since: &since,
}}
} else {
filters = []nostr.Filter{{
Kinds: []int{
nostr.KindArticle,
nostr.KindDeletion,
nostr.KindFollowList,
nostr.KindEncryptedDirectMessage,
nostr.KindMuteList,
nostr.KindRelayListMetadata,
nostr.KindRepost,
nostr.KindZapRequest,
nostr.KindZap,
nostr.KindTextNote,
},
Since: &since,
}}
}
log.Println("๐ฆ archiving trusted notes...")
eventCount := 0
for ev := range pool.SubMany(timeout, seedRelays, filters) {
eventCount++
// Check GC pressure every 1000 events
if eventCount%1000 == 0 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
if m.NumGC > 0 && eventCount > 1000 {
// If we're doing more than 2 GCs per 1000 events, slow down
gcRate := float64(m.NumGC) / float64(eventCount/1000)
if gcRate > 2.0 {
log.Printf("โ ๏ธ High GC pressure (%.1f GC/1000 events), slowing archive process", gcRate)
time.Sleep(100 * time.Millisecond) // Brief pause
}
}
}
// Use semaphore to limit concurrent goroutines
select {
case archiveEventSemaphore <- struct{}{}:
go func(event nostr.Event) {
defer func() { <-archiveEventSemaphore }()
archiveEvent(ctx, relay, event)
}(*ev.Event)
case <-timeout.Done():
log.Printf("๐ฆ archive timeout reached, processed %d events", eventCount)
return
default:
// If semaphore is full, process synchronously to avoid buildup
archiveEvent(ctx, relay, *ev.Event)
}
}
log.Printf("๐ฆ archived %d trusted notes and discarded %d untrusted notes (processed %d total events)",
atomic.LoadUint64(&trustedNotes), atomic.LoadUint64(&untrustedNotes), eventCount)
} else {
log.Println("๐ web of trust will refresh in", config.RefreshInterval, "hours")
select {
case <-timeout.Done():
}
}
}()
select {
case <-timeout.Done():
log.Println("restarting process")
case <-done:
log.Println("๐ฆ archiving process completed")
}
}
func archiveEvent(ctx context.Context, relay *khatru.Relay, ev nostr.Event) {
trustNetworkMutex.RLock()
trusted := trustNetworkMap[ev.PubKey]
trustNetworkMutex.RUnlock()
if trusted {
wdb.Publish(ctx, ev)
relay.BroadcastEvent(&ev)
atomic.AddUint64(&trustedNotes, 1)
atomic.AddUint64(&archivedEvents, 1)
} else {
atomic.AddUint64(&untrustedNotes, 1)
}
}
func deleteOldNotes(relay *khatru.Relay) error {
ctx := context.TODO()
if config.MaxAgeDays <= 0 {
log.Printf("MAX_AGE_DAYS disabled")
return nil
}
maxAgeSecs := nostr.Timestamp(config.MaxAgeDays * 86400)
oldAge := nostr.Now() - maxAgeSecs
if oldAge <= 0 {
log.Printf("MAX_AGE_DAYS too large")
return nil
}
filter := nostr.Filter{
Until: &oldAge,
Kinds: []int{
nostr.KindArticle,
nostr.KindDeletion,
nostr.KindFollowList,
nostr.KindEncryptedDirectMessage,
nostr.KindMuteList,
nostr.KindReaction,
nostr.KindRelayListMetadata,
nostr.KindRepost,
nostr.KindZapRequest,
nostr.KindZap,
nostr.KindTextNote,
},
Limit: 1000, // Process in batches to avoid memory issues
}
ch, err := relay.QueryEvents[0](ctx, filter)
if err != nil {
log.Printf("query error %s", err)
return err
}
// Process events in batches to avoid memory issues
batchSize := 100
events := make([]*nostr.Event, 0, batchSize)
count := 0
for evt := range ch {
events = append(events, evt)
count++
if len(events) >= batchSize {
// Delete this batch
for num_evt, del_evt := range events {
for _, del := range relay.DeleteEvent {
if err := del(ctx, del_evt); err != nil {
log.Printf("error deleting note %d of batch. event id: %s", num_evt, del_evt.ID)
return err
}
}
}
events = events[:0] // Reset slice but keep capacity
}
}
// Delete remaining events
if len(events) > 0 {
for num_evt, del_evt := range events {
for _, del := range relay.DeleteEvent {
if err := del(ctx, del_evt); err != nil {
log.Printf("error deleting note %d of final batch. event id: %s", num_evt, del_evt.ID)
return err
}
}
}
}
if count == 0 {
log.Println("0 old notes found")
} else {
log.Printf("%d old (until %d) notes deleted", count, oldAge)
}
return nil
}
func getDB() badger.BadgerBackend {
return badger.BadgerBackend{
Path: getEnv("DB_PATH"),
}
}
func splitAndTrim(input string) []string {
items := strings.Split(input, ",")
for i, item := range items {
items[i] = strings.TrimSpace(item)
}
return items
}
func isIgnored(pubkey string, ignoredPubkeys []string) bool {
for _, ignored := range ignoredPubkeys {
if pubkey == ignored {
return true
}
}
return false
}
// Add memory monitoring
func monitorMemoryUsage() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
var m runtime.MemStats
runtime.ReadMemStats(&m)
relayMutex.RLock()
relayCount := len(relays)
relayMutex.RUnlock()
trustNetworkMutex.RLock()
trustNetworkCount := len(trustNetwork)
trustNetworkMutex.RUnlock()
oneHopMutex.RLock()
oneHopCount := len(oneHopNetwork)
oneHopMutex.RUnlock()
followerMutex.RLock()
followerCount := len(pubkeyFollowerCount)
followerMutex.RUnlock()
log.Printf("๐ Memory: Alloc=%d KB, Sys=%d KB, NumGC=%d",
m.Alloc/1024, m.Sys/1024, m.NumGC)
log.Printf("๐ Data structures: Relays=%d, TrustNetwork=%d, OneHop=%d, Followers=%d",
relayCount, trustNetworkCount, oneHopCount, followerCount)
}
}
}
// Add performance monitoring
func monitorPerformance() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
var lastGC uint32
var lastEvents, lastRejected, lastArchived uint64
for {
select {
case <-ticker.C:
var m runtime.MemStats
runtime.ReadMemStats(&m)
currentEvents := atomic.LoadUint64(&totalEvents)
currentRejected := atomic.LoadUint64(&rejectedEvents)
currentArchived := atomic.LoadUint64(&archivedEvents)
eventsPerMin := currentEvents - lastEvents
rejectedPerMin := currentRejected - lastRejected
archivedPerMin := currentArchived - lastArchived
gcPerMin := m.NumGC - lastGC
numGoroutines := runtime.NumGoroutine()
log.Printf("โก Performance: Events/min=%d, Rejected/min=%d, Archived/min=%d, GC/min=%d, Goroutines=%d",
eventsPerMin, rejectedPerMin, archivedPerMin, gcPerMin, numGoroutines)
if gcPerMin > 60 {
log.Printf("โ ๏ธ HIGH GC ACTIVITY: %d garbage collections in last minute!", gcPerMin)
}
if numGoroutines > 1000 {
log.Printf("โ ๏ธ HIGH GOROUTINE COUNT: %d goroutines active!", numGoroutines)
}
lastGC = m.NumGC
lastEvents = currentEvents
lastRejected = currentRejected
lastArchived = currentArchived
}
}
}
// Debug handlers
func debugStatsHandler(w http.ResponseWriter, r *http.Request) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
stats := fmt.Sprintf(`Debug Statistics:
Memory:
Allocated: %d KB
System: %d KB
Total Allocations: %d
GC Cycles: %d
Goroutines: %d
Events:
Total Events: %d
Rejected Events: %d
Archived Events: %d
Trusted Notes: %d
Untrusted Notes: %d
Refreshes:
Profile Refreshes: %d
Network Refreshes: %d
Data Structures:
Relays: %d
Trust Network: %d
One Hop Network: %d
Follower Count Map: %d
`,
m.Alloc/1024,
m.Sys/1024,
m.Mallocs,
m.NumGC,
runtime.NumGoroutine(),
atomic.LoadUint64(&totalEvents),
atomic.LoadUint64(&rejectedEvents),
atomic.LoadUint64(&archivedEvents),
atomic.LoadUint64(&trustedNotes),
atomic.LoadUint64(&untrustedNotes),
atomic.LoadUint64(&profileRefreshCount),
atomic.LoadUint64(&networkRefreshCount),
len(relays),
len(trustNetwork),
len(oneHopNetwork),
len(pubkeyFollowerCount),
)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(stats))
}
func debugGoroutinesHandler(w http.ResponseWriter, r *http.Request) {
buf := make([]byte, 1<<20) // 1MB buffer
stackSize := runtime.Stack(buf, true)
w.Header().Set("Content-Type", "text/plain")
w.Write(buf[:stackSize])
}