Reticulum-Go/pkg/cli/slow.go

208 lines
6 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package cli
import (
"encoding/hex"
"flag"
"fmt"
"time"
"quad4/reticulum-go/pkg/rnsutil"
"quad4/reticulum-go/pkg/transport"
)
func RunSlow(args []string, opt ...Options) int {
stdout, stderr := cliIO(opt)
fs := flag.NewFlagSet("rgoslow", flag.ContinueOnError)
fs.SetOutput(stderr)
configDir := fs.String("config", "", "path to config directory")
jsonOut := fs.Bool("json", false, "emit JSON report")
all := fs.Bool("a", false, "include hidden/local peer interfaces")
nameFilter := fs.String("n", "", "filter interfaces by name substring")
links := fs.Bool("l", false, "include active link count")
destHex := fs.String("dest", "", "focus analysis on destination hash (32 hex chars)")
topN := fs.Int("top", 12, "max interfaces to rank")
highHop := fs.Int("high-hop", 6, "hop count treated as high")
timeout := fs.Duration("timeout", 30*time.Second, "RPC timeout")
monitor := fs.Bool("m", false, "continuously refresh report")
interval := fs.Duration("I", 2*time.Second, "monitor refresh interval")
quiet := fs.Bool("q", false, "quiet: suppress stderr hints")
withPaths := fs.Bool("paths", false, "include full path-table analysis (can be slow on large tables)")
fs.Usage = func() {
fmt.Fprintf(stderr, `rgoslow - find interfaces, paths, and transports slowing transfers
Queries a running shared instance (Go reticulum-go or Python rnsd) over RPC
and ranks congestion signals that commonly stall resource transfers:
· bitrate caps and utilization
· announce / path-request bursts and held announces
· bandwidth gates
· socket RTT (Go daemon)
· high-hop paths and congested egress hubs
Usage:
rgoslow [flags]
reticulum-go slow [flags]
Flags:
`)
fs.PrintDefaults()
fmt.Fprintf(stderr, `
Examples:
rgoslow
rgoslow -config ~/.reticulum
rgoslow -dest 06a54b505bb67b25ef3f8097e8001edc
rgoslow -json -l
rgoslow -paths
rgoslow -m -I 3s
`)
}
if err := fs.Parse(args); err != nil {
return 2
}
cfg, err := rnsutil.LoadConfigDir(*configDir)
if err != nil {
fmt.Fprintf(stderr, "config: %v\n", err)
return 1
}
client, err := rnsutil.DialRPC(cfg, nil)
if err != nil {
fmt.Fprintf(stderr, "%s: %v\n", errMsg(stderr, "rpc"), err)
if !*quiet {
fmt.Fprintln(stderr, warnMsg(stderr, "hint: point -config at the daemon config dir (e.g. ~/.reticulum for rnsd)"))
fmt.Fprintln(stderr, warnMsg(stderr, "hint: align shared_instance_type (unix on Linux by default, or tcp) and instance_name / ports"))
}
return 1
}
client.SetTimeout(*timeout)
analyzeOpts := rnsutil.SlowAnalyzeOptions{
HighHopThreshold: *highHop,
TopN: *topN,
NameFilter: *nameFilter,
ShowAll: *all,
}
runOnce := func() int {
stats, err := client.GetInterfaceStats()
if err != nil {
fmt.Fprintf(stderr, "%s (%s): %v\n", errMsg(stderr, "interface stats"), client.Addr(), err)
if !*quiet {
fmt.Fprintln(stderr, warnMsg(stderr, "hint: start rnsd or reticulum-go first, then retry"))
}
return 1
}
var paths []transport.PathTableEntry
if *withPaths {
paths, err = client.GetPathTable(nil)
if err != nil {
fmt.Fprintf(stderr, "%s: %v\n", warnMsg(stderr, "path table"), err)
if !*quiet {
fmt.Fprintln(stderr, warnMsg(stderr, "hint: continuing with interface analysis only"))
}
paths = nil
}
}
var linkCount *int
if *links {
n, err := client.GetLinkCount()
if err != nil {
fmt.Fprintf(stderr, "link count: %v\n", err)
return 1
}
linkCount = &n
}
rep := rnsutil.AnalyzeSlow(stats, paths, linkCount, client.Addr(), analyzeOpts)
if !*withPaths {
rep.Recommendations = append([]string{
"Path-table scan skipped (default). Use -paths for hop/egress hotspots, or -dest <hash> for one destination",
}, rep.Recommendations...)
}
if *destHex != "" {
focus, err := focusDestination(client, paths, *destHex)
if err != nil {
fmt.Fprintf(stderr, "dest: %v\n", err)
return 1
}
rep.Destination = focus
if focus.HasPath && focus.Interface != "" {
for _, row := range rep.Interfaces {
if row.Name == focus.Interface && row.Score >= 25 {
rep.Recommendations = append([]string{
fmt.Sprintf("Destination egress %s is constrained: %s", row.Name, row.Why),
}, rep.Recommendations...)
break
}
}
if focus.Hops >= analyzeOpts.HighHopThreshold {
rep.Recommendations = append([]string{
fmt.Sprintf("This destination is %d hops away: resource chunks need many round trips", focus.Hops),
}, rep.Recommendations...)
}
}
}
if *jsonOut {
if err := rnsutil.WriteSlowJSON(stdout, rep); err != nil {
fmt.Fprintf(stderr, "json: %v\n", err)
return 1
}
return 0
}
if err := rnsutil.WriteSlowHuman(stdout, rep); err != nil {
fmt.Fprintf(stderr, "write: %v\n", err)
return 1
}
return 0
}
if !*monitor {
return runOnce()
}
for {
if code := runOnce(); code != 0 {
return code
}
time.Sleep(*interval)
fmt.Fprintln(stdout)
}
}
func focusDestination(client *rnsutil.RPCClient, paths []transport.PathTableEntry, destHex string) (*rnsutil.DestFocus, error) {
raw, err := hex.DecodeString(destHex)
if err != nil || len(raw) != 16 {
return nil, fmt.Errorf("destination hash must be 32 hex characters")
}
focus := &rnsutil.DestFocus{Hash: rnsutil.PrettyHex(raw)}
now := float64(time.Now().Unix())
for _, p := range paths {
if len(p.Hash) == 16 && string(p.Hash) == string(raw) {
focus.HasPath = true
focus.Hops = int(p.Hops)
focus.Via = rnsutil.PrettyHex(p.Via)
focus.Interface = p.Interface
if p.Expires > now {
focus.ExpiresInS = p.Expires - now
}
break
}
}
if to, err := client.GetFirstHopTimeout(raw); err == nil {
focus.FirstHopTimeoutS = to
}
if nhIf, err := client.GetNextHopIfName(raw); err == nil && nhIf != "" {
focus.Interface = nhIf
focus.HasPath = true
}
if via, err := client.GetNextHop(raw); err == nil && len(via) > 0 {
focus.Via = rnsutil.PrettyHex(via)
focus.HasPath = true
}
return focus, nil
}