From fdbf9e15cc9005f1000e500581361ec265fad61d Mon Sep 17 00:00:00 2001 From: Suphanat Chunhapanya Date: Fri, 17 Oct 2025 19:26:49 +0700 Subject: [PATCH] Enhance and plot simnet simulations --- .gitignore | 10 ++ CLAUDE.md | 98 +++++++++++++++++-- Makefile | 30 ++++-- README.md | 118 ++++++++++++++++++----- gossipsub/simnet_test.go | 72 ++++++++++---- networkinfo/networkinfo.go | 1 + networkinfo/types.go | 1 + plot_simnet_propagation.py | 178 +++++++++++++++++++++++++++++++++++ shadowconfig/shadowconfig.go | 17 ++++ 9 files changed, 470 insertions(+), 55 deletions(-) create mode 100644 plot_simnet_propagation.py diff --git a/.gitignore b/.gitignore index 47aa3a5..b731cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ gossipsub/topology.json shadow-gossipsub.yaml message_propagation.png +# Simnet simulation outputs +simnet.log +simnet_propagation.png + # Go build artifacts *.out @@ -23,3 +27,9 @@ message_propagation.png # Coverage files coverage.out *.coverprofile + +# Python virtual environment +.venv/ +__pycache__/ +*.pyc +*.pyo diff --git a/CLAUDE.md b/CLAUDE.md index 591746c..f25eb22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a GossipSub simulation framework for testing Ethereum attestation propagation using the Shadow discrete-event network simulator. The project simulates libp2p GossipSub networks with realistic network conditions and Ethereum consensus-specs parameters. +This is a GossipSub simulation framework for testing Ethereum attestation propagation. The project supports two simulation approaches: + +1. **Shadow Simulator**: Discrete-event network simulator that runs the actual GossipSub binary in a simulated network environment +2. **Simnet**: Go's deterministic network simulation using `testing/synctest` for fast, reproducible tests + +Both approaches simulate libp2p GossipSub networks with realistic network conditions and Ethereum consensus-specs parameters. ## Architecture @@ -15,12 +20,23 @@ This is a GossipSub simulation framework for testing Ethereum attestation propag - **generators.go**: Three topology generators (mesh, tree, random-regular) - **gen/main.go**: CLI tool for generating topology files -- **gossipsub/**: GossipSub simulation binary - - **main.go**: Simulation entry point using go-libp2p-pubsub +- **gossipsub/**: GossipSub simulation binaries and tests + - **main.go**: Shadow simulation entry point using go-libp2p-pubsub + - **simnet_test.go**: Simnet test using Go 1.25+ testing/synctest - Implements Ethereum consensus-specs GossipSub parameters - Uses deterministic peer IDs for reproducibility - All nodes publish one message each (N nodes → N messages total) +- **networkinfo/**: Network configuration parser + - **networkinfo.go**: Parses Shadow YAML and GML graph files + - **types.go**: Data structures for host network information + - Extracts bandwidth, latency, and message size per host + +- **shadowconfig/**: Shadow configuration parser + - **shadowconfig.go**: Parses Shadow YAML configuration + - Extracts node network IDs and process arguments + - Used by simnet to configure network conditions + - **network_graph.py**: Shadow configuration generator - Generates realistic network graphs with global latencies - Distributes nodes across geographic locations @@ -31,11 +47,16 @@ This is a GossipSub simulation framework for testing Ethereum attestation propag - Checks that all nodes received the message - Used in CI tests -- **plot_propagation.py**: Message propagation visualization +- **plot_propagation.py**: Shadow message propagation visualization - Parses Shadow logs to extract message reception timestamps - Plots average messages per node over time - Generates message_propagation.png +- **plot_simnet_propagation.py**: Simnet message propagation visualization + - Parses simnet.log to extract message reception timestamps + - Plots average messages per node over time + - Generates simnet_propagation.png + - **shadow.template.yaml**: Shadow configuration template - **pyproject.toml**: Python project configuration @@ -65,19 +86,28 @@ Python dependencies defined in `pyproject.toml`: ## Development Commands ### Make Commands (Primary Interface) + +**Shadow Simulation:** ```bash make help # Show all available commands -make all # Build, run simulation, test, and plot (default) +make all # Build, run Shadow simulation, test, and plot (default) make build # Build GossipSub binary make build-topology-gen # Build topology generator make generate-topology # Generate topology file make generate-config # Generate Shadow configuration make run-sim # Run complete Shadow simulation -make test # Test simulation results -make plot # Plot message propagation over time +make test # Test Shadow simulation results +make plot # Plot Shadow message propagation over time make clean # Clean build artifacts and results ``` +**Simnet Simulation:** +```bash +make run-simnet # Run simnet test (requires Go 1.25+) +make plot-simnet # Plot simnet message propagation +make all-simnet # Run simnet and plot (complete workflow) +``` + ### Configuration Variables ```bash # Topology settings @@ -130,7 +160,9 @@ uv run plot_propagation.py 10 shadow shadow-gossipsub.yaml ``` -## Simulation Workflow +## Simulation Workflows + +### Shadow Simulation Workflow 1. **Topology Generation** - Makefile generates topology.json using topology-gen @@ -144,6 +176,7 @@ shadow shadow-gossipsub.yaml - Geographic distribution: Australia, Europe, Asia, Americas, Africa 3. **GossipSub Simulation** + - Shadow runs gossipsub binary for each node in simulated network - Each node loads topology and creates libp2p host - Deterministic peer IDs (seed-based key generation) - All nodes start synchronized at 2000/01/01 00:02:00 @@ -156,6 +189,45 @@ shadow shadow-gossipsub.yaml - Verifies all nodes received all N messages - Reports pass/fail for each node +### Simnet Simulation Workflow + +1. **Configuration Loading** + - Simnet test reads shadow-gossipsub.yaml and graph.gml + - Parses network parameters (bandwidth, latency, message size) + - Loads topology.json for peer connections + - Requires Go 1.25+ for testing/synctest support + +2. **Network Setup** + - Creates simulated network using simlibp2p and simnet packages + - Configures per-host bandwidth and latency based on Shadow config + - Uses IP-to-host-ID mapping for latency calculations + - Latency applied on downlink (receiver adds latency based on sender) + +3. **GossipSub Test Execution** + - Creates libp2p hosts for all nodes in deterministic simulation + - Establishes connections based on topology + - All nodes subscribe to test topic + - Every node publishes one message + - Tracks received message count per node using atomic counters + +4. **Result Validation** + - Test verifies each node received exactly N messages + - Logs reception timestamps to simnet.log for plotting + - Test fails if any node didn't receive all messages + - Validates complete message propagation across the network + +## Shadow vs Simnet + +| Aspect | Shadow | Simnet | +|--------|--------|--------| +| **Execution** | Discrete-event simulator, runs actual binaries | Go test using testing/synctest | +| **Speed** | Slower (minutes for 10 nodes) | Faster (seconds for 20 nodes) | +| **Realism** | High (realistic network, process isolation) | Medium (simulated network in memory) | +| **Debugging** | Harder (separate processes) | Easier (single process, Go debugger) | +| **Requirements** | Shadow v3.2.0+ | Go 1.25+ | +| **Use Case** | Final validation, realistic scenarios | Development, quick iteration | +| **Determinism** | Deterministic with same config | Fully deterministic (synctest) | + ## GossipSub Parameters The simulation uses Ethereum consensus-specs parameters: @@ -254,17 +326,27 @@ make all - shimlog: Shadow internal logs ### Troubleshooting + +**Shadow:** - **"Topology file is required"**: Makefile generates this automatically - **"cannot find binary path"**: Check gossipsub/gossipsub exists - **Shadow simulation hangs**: Check Shadow version (requires v3.2.0) - **All nodes didn't receive message**: Check Shadow logs for connection errors +**Simnet:** +- **"no test files" or build constraint error**: Requires Go 1.25+ for testing/synctest +- **Test fails with wrong message count**: Check simnet.log for errors, ensure topology is connected +- **Network latency errors**: Verify shadow-gossipsub.yaml and graph.gml are generated +- **Plot script fails**: Run `make run-simnet` first to generate simnet.log + ## Module Structure The project uses `github.com/ethp2p/attsim` with key dependencies: - go-libp2p v0.41.1 - go-libp2p-pubsub v0.15.0 - go-log/v2 v2.8.2 +- github.com/marcopolo/simnet (for simnet simulation) +- testing/synctest (Go 1.25+ standard library, for deterministic testing) ## Related Projects diff --git a/Makefile b/Makefile index 47ff9f6..6a604a1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for attsim GossipSub Shadow simulation -.PHONY: all build build-topology-gen generate-topology generate-config run-sim test plot clean help check-deps +.PHONY: all all-simnet build build-topology-gen generate-topology generate-config run-sim run-simnet test plot plot-simnet clean help check-deps # Default parameters NODE_COUNT ?= 10 @@ -58,10 +58,10 @@ run-sim: build generate-config shadow --progress $(PROGRESS) shadow-gossipsub.yaml @echo "GossipSub simulation completed" -# Run the complete Shadow simulation +# Run the complete simnet simulation run-simnet: generate-config @echo "Starting GossipSub simnet simulation ($(NODE_COUNT) nodes, $(MSG_SIZE) byte message)..." - cd $(GOSSIPSUB_DIR) && go test -v . + cd $(GOSSIPSUB_DIR) && go test -v . 2>&1 | tee ../simnet.log @echo "GossipSub simnet simulation completed" # Test simulation results @@ -75,6 +75,13 @@ plot: uv run plot_propagation.py $(NODE_COUNT) @test -f message_propagation.png && echo "Plot generated: message_propagation.png" || echo "Plot generation failed" +# Plot simnet message propagation +plot-simnet: + @echo "Plotting simnet message propagation..." + @test -f simnet.log || (echo "Error: simnet.log not found. Run 'make run-simnet' first" && exit 1) + uv run plot_simnet_propagation.py simnet.log + @test -f simnet_propagation.png && echo "Plot generated: simnet_propagation.png" || echo "Plot generation failed" + # Clean build artifacts and simulation results clean: @echo "Cleaning up..." @@ -83,6 +90,8 @@ clean: rm -f shadow-gossipsub.yaml rm -f graph.gml rm -f message_propagation.png + rm -f simnet_propagation.png + rm -f simnet.log rm -rf shadow.data/ rm -f $(TOPOLOGY_GEN_DIR)/topology-gen @echo "Cleanup completed" @@ -90,6 +99,9 @@ clean: # Complete workflow: build, run, test, and plot all: run-sim test plot +# Complete simnet workflow: run and plot +all-simnet: run-simnet plot-simnet + # Help target help: @echo "attsim GossipSub Shadow Simulation" @@ -102,9 +114,12 @@ help: @echo " build - Build the GossipSub simulation binary" @echo " generate-config - Generate Shadow configuration" @echo " run-sim - Run complete Shadow simulation" + @echo " run-simnet - Run complete simnet simulation (requires Go 1.25+)" @echo " test - Test simulation results" - @echo " plot - Plot message propagation over time" - @echo " all - Run simulation, test, and plot (default)" + @echo " plot - Plot message propagation over time (Shadow)" + @echo " plot-simnet - Plot message propagation over time (simnet)" + @echo " all - Run Shadow simulation, test, and plot (default)" + @echo " all-simnet - Run simnet simulation and plot" @echo " clean - Clean up build artifacts and results" @echo " help - Show this help message" @echo "" @@ -118,7 +133,8 @@ help: @echo " LOG_LEVEL - Log level (default: $(LOG_LEVEL))" @echo "" @echo "Examples:" - @echo " make all # Run simulation, test, and plot (random-regular)" + @echo " make all # Run Shadow simulation, test, and plot" + @echo " make all-simnet # Run simnet simulation and plot" @echo " make run-sim NODE_COUNT=20 MSG_SIZE=512 # Custom parameters" @echo " make run-sim TOPOLOGY_TYPE=mesh NODE_COUNT=10 # Mesh topology" @echo " make run-sim TOPOLOGY_TYPE=tree NODE_COUNT=31 BRANCHING=2 # Tree topology" @@ -126,3 +142,5 @@ help: @echo " make run-sim PROGRESS=true # Run with progress bar" @echo " make test # Test existing simulation results" @echo " make plot NODE_COUNT=10 # Plot message propagation" + @echo " make run-simnet NODE_COUNT=10 # Run simnet simulation" + @echo " make plot-simnet # Plot simnet propagation" diff --git a/README.md b/README.md index acaefae..142624c 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,34 @@ # attsim - Attestation Simulation -A GossipSub simulation framework for testing Ethereum attestation propagation using the Shadow discrete-event network simulator. +A GossipSub simulation framework for testing Ethereum attestation propagation. Supports both Shadow discrete-event network simulation and Simnet deterministic testing. ## Overview -This repository provides tools to simulate GossipSub-based peer-to-peer networks using Shadow, focusing on realistic network conditions and Ethereum consensus layer parameters. +This repository provides tools to simulate GossipSub-based peer-to-peer networks with realistic network conditions and Ethereum consensus layer parameters. + +**Simulation Methods:** +1. **Shadow** - Discrete-event network simulator for realistic, large-scale simulations +2. **Simnet** - Fast, deterministic Go tests using `testing/synctest` (Go 1.25+) **Key Features:** - libp2p GossipSub implementation with Ethereum consensus-specs parameters - Multiple network topology generators (mesh, tree, random-regular) -- Shadow discrete-event network simulation - Deterministic peer IDs and reproducible simulations +- Message propagation visualization and validation ## Prerequisites ### Required -- **Go 1.23+** - for building simulation binaries -- **Shadow** - discrete-event network simulator ([installation guide](https://shadow.github.io/docs/guide/supported_platforms.html)) +- **Go 1.23+** - for building Shadow simulation binaries +- **Go 1.25+** - for running Simnet tests (optional, uses `testing/synctest`) - **Python 3.12+** - for configuration generation and visualization - **uv** - Python package manager ([installation guide](https://docs.astral.sh/uv/getting-started/installation/)) +### Optional (for Shadow simulation) + +- **Shadow v3.2.0+** - discrete-event network simulator ([installation guide](https://shadow.github.io/docs/guide/supported_platforms.html)) + ```bash # Install uv (if not already installed) curl -LsSf https://astral.sh/uv/install.sh | sh @@ -44,8 +52,10 @@ uv --version ## Quick Start +### Shadow Simulation + ```bash -# Run simulation with default parameters (random-regular topology, 10 nodes) +# Run Shadow simulation with default parameters (random-regular topology, 10 nodes) # This runs the simulation, tests results, and generates a propagation plot make all @@ -53,21 +63,42 @@ make all make run-sim NODE_COUNT=20 MSG_SIZE=512 TOPOLOGY_TYPE=mesh ``` +### Simnet Testing (Go 1.25+) + +```bash +# Run simnet test and generate plot +make all-simnet + +# Or run and plot separately +make run-simnet # Run the test +make plot-simnet # Generate propagation plot +``` + ## Directory Structure ``` attsim/ -├── README.md # This file -├── Makefile # Build and run commands -├── network_graph.py # Shadow network configuration generator -├── shadow.template.yaml # Shadow configuration template -├── topology/ # Network topology generation -│ ├── topology.go # Topology data structures -│ ├── generators.go # Topology generation algorithms -│ └── gen/ # Topology generator CLI tool +├── README.md # This file +├── CLAUDE.md # Project documentation for Claude Code +├── Makefile # Build and run commands +├── network_graph.py # Shadow network configuration generator +├── plot_propagation.py # Shadow message propagation plotter +├── plot_simnet_propagation.py # Simnet message propagation plotter +├── test_results.py # Shadow simulation test harness +├── shadow.template.yaml # Shadow configuration template +├── topology/ # Network topology generation +│ ├── topology.go # Topology data structures +│ ├── generators.go # Topology generation algorithms +│ └── gen/ # Topology generator CLI tool │ └── main.go -└── gossipsub/ # GossipSub simulation - └── main.go # Simulation entry point +├── networkinfo/ # Network configuration parser +│ ├── networkinfo.go # Shadow YAML and GML parser +│ └── types.go # Network info data structures +├── shadowconfig/ # Shadow config parser +│ └── shadowconfig.go # Shadow YAML parser +└── gossipsub/ # GossipSub simulation + ├── main.go # Shadow simulation entry point + └── simnet_test.go # Simnet deterministic test ``` ## Usage @@ -94,13 +125,21 @@ make run-sim PROGRESS=true ### Makefile Targets -- `make all` - Complete workflow (build, generate topology, run simulation, test, plot) +**Shadow Simulation:** +- `make all` - Complete Shadow workflow (build, generate topology, run simulation, test, plot) - `make build` - Build the GossipSub binary - `make generate-topology` - Generate topology file - `make generate-config` - Generate Shadow configuration -- `make run-sim` - Run complete simulation -- `make test` - Test simulation results -- `make plot` - Plot message propagation over time +- `make run-sim` - Run Shadow simulation +- `make test` - Test Shadow simulation results +- `make plot` - Plot Shadow message propagation over time + +**Simnet Testing:** +- `make all-simnet` - Complete Simnet workflow (run test and plot) +- `make run-simnet` - Run simnet test (requires Go 1.25+) +- `make plot-simnet` - Plot simnet message propagation + +**General:** - `make clean` - Clean up artifacts and results - `make help` - Show help with all options @@ -202,9 +241,22 @@ make plot make clean ``` +## Simulation Comparison + +| Aspect | Shadow | Simnet | +|--------|--------|--------| +| **Execution** | Discrete-event simulator, runs actual binaries | Go test using testing/synctest | +| **Speed** | Slower (minutes for 10 nodes) | Faster (seconds for 20 nodes) | +| **Realism** | High (realistic network, process isolation) | Medium (simulated network in memory) | +| **Debugging** | Harder (separate processes) | Easier (single process, Go debugger) | +| **Requirements** | Shadow v3.2.0+ | Go 1.25+ | +| **Use Case** | Final validation, realistic scenarios | Development, quick iteration | +| **Output** | Shadow logs in shadow.data/ | simnet.log | +| **Visualization** | plot_propagation.py | plot_simnet_propagation.py | + ## Architecture -### Simulation Workflow +### Shadow Simulation Workflow 1. **Topology Generation** - The Makefile automatically generates a topology file based on parameters @@ -223,6 +275,30 @@ make clean - Every node publishes one message after network stabilization - All nodes receive and log all messages (N messages for N nodes) +### Simnet Test Workflow + +1. **Configuration Loading** + - Simnet test reads shadow-gossipsub.yaml and graph.gml + - Parses network parameters (bandwidth, latency, message size) + - Loads topology.json for peer connections + +2. **Network Setup** + - Creates simulated network using simlibp2p and simnet packages + - Configures per-host bandwidth and latency based on Shadow config + - Latency applied on downlink (receiver adds latency based on sender) + +3. **GossipSub Test Execution** + - Creates libp2p hosts for all nodes in deterministic simulation + - Establishes connections based on topology + - All nodes subscribe to test topic + - Every node publishes one message + - Tracks received message count per node using atomic counters + +4. **Validation** + - Test verifies each node received exactly N messages + - Logs reception timestamps to simnet.log for plotting + - Test fails if any node didn't receive all messages + ### Topology JSON Format ```json diff --git a/gossipsub/simnet_test.go b/gossipsub/simnet_test.go index 25c2bfc..04305e6 100644 --- a/gossipsub/simnet_test.go +++ b/gossipsub/simnet_test.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net" + "sync/atomic" "testing" "testing/synctest" "time" @@ -199,39 +200,70 @@ func TestGossipSubWithSimnet(t *testing.T) { t.Log("Waiting for network to stabilize...") time.Sleep(2 * time.Second) - // Publish a message from host 0 - testMsg := []byte("Hello from simnet GossipSub!") - if err := topics[0].Publish(ctx, testMsg); err != nil { - t.Fatalf("Failed to publish message: %v", err) - } - t.Log("Published test message from host 0") - - // Wait for message propagation - receivedCount := 0 + // Track received message counts per node + receivedCount := make([]atomic.Int64, nodeCount) - for i := 1; i < nodeCount; i++ { + // Start message receivers for all nodes + publishTime := time.Now() + for i := range nodeCount { go func(idx int) { for { - msg, err := subs[idx].Next(ctx) + _, err := subs[idx].Next(ctx) if err != nil { return } - if string(msg.Data) == string(testMsg) { - receivedCount++ - t.Logf("Host %d received message", idx) - return - } + receivedCount[idx].Add(1) + receiveTime := time.Now() + elapsedMs := float64(receiveTime.Sub(publishTime).Microseconds()) / 1000.0 + t.Logf("SIMNET_RECEIVE: time=%s host=%d elapsed_ms=%.3f", + receiveTime.UTC().Format(time.RFC3339Nano), idx, elapsedMs) } }(i) } - // Wait for all messages to be received + // All nodes publish a message (like Shadow simulation) + for i := range nodeCount { + hostMsgSize := netInfo.Hosts[i].MsgSize + if hostMsgSize == 0 { + t.Fatalf("Message size not set for host %d", i) + } + + // Create message for this node + testMsg := make([]byte, hostMsgSize) + msgHeader := []byte(fmt.Sprintf("Message-from-node-%d", i)) + copy(testMsg, msgHeader) + // Fill remaining bytes with pattern + for j := len(msgHeader); j < hostMsgSize; j++ { + testMsg[j] = byte('A' + (j % 26)) + } + + pubTime := time.Now() + if err := topics[i].Publish(ctx, testMsg); err != nil { + t.Fatalf("Failed to publish message from host %d: %v", i, err) + } + t.Logf("SIMNET_PUBLISH: time=%s host=%d", pubTime.UTC().Format(time.RFC3339Nano), i) + } + + // Wait for message propagation (N nodes * N messages) time.Sleep(5 * time.Second) - if receivedCount != nodeCount-1 { - t.Fatalf("Expected %d hosts to receive message, got %d", nodeCount-1, receivedCount) + // Verify all nodes received all N messages + t.Log("Verifying message reception...") + allPassed := true + for i := range nodeCount { + count := receivedCount[i].Load() + if count != int64(nodeCount) { + t.Errorf("Node %d received %d messages, expected %d", i, count, nodeCount) + allPassed = false + } else { + t.Logf("Node %d: received %d/%d messages ✓", i, count, nodeCount) + } + } + + if !allPassed { + t.Fatal("Not all nodes received the expected number of messages") } - t.Log("All hosts successfully received the message!") + t.Log("Simulation completed!") }) } diff --git a/networkinfo/networkinfo.go b/networkinfo/networkinfo.go index 93299a8..5625f62 100644 --- a/networkinfo/networkinfo.go +++ b/networkinfo/networkinfo.go @@ -47,6 +47,7 @@ func Parse(shadowConfigPath, gmlPath string) (*NetworkInfo, error) { NetworkNodeID: shadowNode.NetworkNodeID, BandwidthUpBps: networkNode.BandwidthUpBps, BandwidthDownBps: networkNode.BandwidthDownBps, + MsgSize: shadowNode.MsgSize, } } diff --git a/networkinfo/types.go b/networkinfo/types.go index ff7e154..5b4cfa6 100644 --- a/networkinfo/types.go +++ b/networkinfo/types.go @@ -6,6 +6,7 @@ type HostNetworkInfo struct { NetworkNodeID int // Corresponding network node ID in the GML graph BandwidthUpBps uint64 // Upload bandwidth in bits per second BandwidthDownBps uint64 // Download bandwidth in bits per second + MsgSize int // Message size in bytes } // NetworkInfo contains complete network information for all hosts diff --git a/plot_simnet_propagation.py b/plot_simnet_propagation.py new file mode 100644 index 0000000..297db82 --- /dev/null +++ b/plot_simnet_propagation.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Plot message propagation over time from simnet test results. + +Plots the number of received messages by all nodes divided by the number of nodes +as a function of time, showing how messages propagate through the network. +""" + +import sys +import os +import re +from datetime import datetime +from typing import List, Tuple +import matplotlib.pyplot as plt + + +def parse_timestamp(timestamp_str: str) -> datetime: + """Parse RFC3339Nano timestamp from simnet logs.""" + # Format: 2000-01-01T00:00:05.176Z or 2000-01-01T00:00:05Z + # Python's datetime doesn't support nanoseconds, so we'll truncate to microseconds + if '.' in timestamp_str: + # Split timestamp and fractional seconds + base, frac = timestamp_str.split('.') + # Remove 'Z' from fractional part + frac_digits = frac.rstrip('Z') + # Take only first 6 digits (microseconds) and pad with zeros if needed + frac_us = frac_digits[:6].ljust(6, '0') + timestamp_str = f"{base}.{frac_us}Z" + + return datetime.fromisoformat(timestamp_str.replace('Z', '+00:00')) + + +def parse_simnet_logs(log_file: str) -> Tuple[List[Tuple[float, int]], datetime]: + """ + Parse simnet test logs to extract message reception times. + + Returns: + Tuple of (List of (elapsed_ms, cumulative_count) tuples, publish_time) + """ + events = [] # List of (elapsed_ms, host_id) + publish_time = None + + if not os.path.exists(log_file): + print(f"Error: Log file not found: {log_file}") + return [], None + + with open(log_file, 'r') as f: + for line in f: + # Match SIMNET_PUBLISH: time= host= + publish_match = re.search(r'SIMNET_PUBLISH: time=([^\s]+) host=(\d+)', line) + if publish_match: + timestamp_str = publish_match.group(1) + publish_time = parse_timestamp(timestamp_str) + continue + + # Match SIMNET_RECEIVE: time= host= elapsed_ms= + receive_match = re.search(r'SIMNET_RECEIVE: time=([^\s]+) host=(\d+) elapsed_ms=([\d.]+)', line) + if receive_match: + timestamp_str = receive_match.group(1) + host_id = int(receive_match.group(2)) + elapsed_ms = float(receive_match.group(3)) + events.append((elapsed_ms, host_id)) + + if not events: + return [], publish_time + + # Sort events by elapsed time + events.sort(key=lambda x: x[0]) + + # Calculate cumulative totals + cumulative_data = [] + total_count = 0 + + for elapsed_ms, host_id in events: + total_count += 1 + cumulative_data.append((elapsed_ms, total_count)) + + return cumulative_data, publish_time + + +def plot_propagation(log_file: str, node_count: int): + """Plot message propagation over time from simnet test results.""" + print(f"Parsing simnet logs for {node_count} nodes...") + + cumulative_data, publish_time = parse_simnet_logs(log_file) + + if not cumulative_data: + print("Error: No message reception events found in logs") + return + + if publish_time is None: + print("Warning: Publish time not found in logs") + + # Calculate metric: total_received / node_count + timestamps = [t for t, _ in cumulative_data] + avg_per_node = [count / node_count for _, count in cumulative_data] + + # Create plot + plt.figure(figsize=(10, 6)) + plt.plot(timestamps, avg_per_node, linewidth=2) + plt.xlabel('Time (ms)', fontsize=12) + plt.ylabel('Average messages received per node', fontsize=12) + plt.title(f'Simnet Message Propagation Over Time ({node_count} nodes)', fontsize=14) + plt.grid(True, alpha=0.3) + + # Add horizontal line at expected final value + # All nodes publish, so expected is N messages per node (like Shadow) + expected_final = node_count + plt.axhline(y=expected_final, color='r', linestyle='--', alpha=0.5, label=f'Expected final: {expected_final}') + + plt.legend() + plt.tight_layout() + + # Save plot + output_file = 'simnet_propagation.png' + plt.savefig(output_file, dpi=150) + print(f"\nPlot saved to: {output_file}") + + # Print statistics + if cumulative_data: + start_time = timestamps[0] + end_time = timestamps[-1] + duration = end_time - start_time + final_avg = avg_per_node[-1] + + print(f"\nPropagation Statistics:") + print(f" Start time: {start_time:.3f}ms (after publish)") + print(f" End time: {end_time:.3f}ms (after publish)") + print(f" Duration: {duration:.3f}ms") + print(f" Final average: {final_avg:.2f} messages/node") + print(f" Expected: {expected_final} messages/node") + print(f" Total events: {len(cumulative_data)}") + + if publish_time: + print(f" Publish time: {publish_time.isoformat()}") + + +def main(): + if len(sys.argv) < 2 or len(sys.argv) > 3: + print("Usage: python3 plot_simnet_propagation.py [node-count]") + print(" log-file: Path to simnet test output log file") + print(" node-count: Number of nodes in the simulation (optional, for plotting)") + sys.exit(1) + + log_file = sys.argv[1] + + # Try to determine node count from log file if not provided + node_count = None + if len(sys.argv) == 3: + try: + node_count = int(sys.argv[2]) + except ValueError: + print("Error: node-count must be an integer") + sys.exit(1) + else: + # Try to infer node count from the log file + # Look for "Using N nodes from network configuration" line + if os.path.exists(log_file): + with open(log_file, 'r') as f: + for line in f: + match = re.search(r'Using (\d+) nodes from network configuration', line) + if match: + node_count = int(match.group(1)) + break + + if node_count is None: + print("Error: Could not determine node count. Please provide it as second argument.") + sys.exit(1) + + if node_count <= 0: + print("Error: node-count must be positive") + sys.exit(1) + + plot_propagation(log_file, node_count) + + +if __name__ == "__main__": + main() diff --git a/shadowconfig/shadowconfig.go b/shadowconfig/shadowconfig.go index 3bdd784..3a7c0e4 100644 --- a/shadowconfig/shadowconfig.go +++ b/shadowconfig/shadowconfig.go @@ -14,6 +14,7 @@ import ( type NodeNetworkInfo struct { NodeID int NetworkNodeID int + MsgSize int // Message size in bytes from -msg-size argument } // Process represents a Shadow process configuration @@ -63,9 +64,25 @@ func ParseConfig(filename string) ([]NodeNetworkInfo, error) { return nil, fmt.Errorf("failed to parse node ID from %s: %w", hostName, err) } + // Parse message size from process arguments + var msgSize int + if len(host.Processes) > 0 { + args := strings.Fields(host.Processes[0].Args) + for i := 0; i < len(args)-1; i++ { + if args[i] == "-msg-size" { + msgSize, err = strconv.Atoi(args[i+1]) + if err != nil { + return nil, fmt.Errorf("failed to parse msg-size for %s: %w", hostName, err) + } + break + } + } + } + nodes = append(nodes, NodeNetworkInfo{ NodeID: nodeID, NetworkNodeID: host.NetworkNodeID, + MsgSize: msgSize, }) }