How Traffic Statistics Are Calculated, Tracked, and Displayed in Shadowsocks-Windows
Shadowsocks-Windows tracks network traffic by atomically incrementing byte counters during packet relay, computing per-second deltas in a background thread, and rendering live charts in the Log Viewer window.
Shadowsocks-Windows provides real-time traffic monitoring through its Log Viewer interface, allowing users to visualize network throughput as it happens. Understanding how traffic statistics are calculated, tracked, and displayed requires examining the interplay between the core controller's atomic counters and the UI's chart rendering engine. This article breaks down the complete data flow from raw byte counting to the final visual representation using the actual source code from the shadowsocks/shadowsocks-windows repository.
Counting Raw Bytes at the Controller Level
Traffic measurement begins with two atomic long fields inside ShadowsocksController that record cumulative inbound and outbound bytes. These counters are updated every time TCPRelay or UDPRelay processes network data.
Atomic Counter Implementation
The controller stores raw byte counts as thread-safe fields accessible via Interlocked operations:
private long _inboundCounter = 0;
private long _outboundCounter = 0;
public long InboundCounter => Interlocked.Read(ref _inboundCounter);
public long OutboundCounter => Interlocked.Read(ref _outboundCounter);
When relays transmit data, they invoke UpdateInboundCounter and UpdateOutboundCounter with the packet length. These methods use Interlocked.Add to ensure thread-safe increments without locking:
public void UpdateInboundCounter(object sender, SSTransmitEventArgs args)
{
GetCurrentStrategy()?.UpdateLastRead(args.server);
Interlocked.Add(ref _inboundCounter, args.length);
}
public void UpdateOutboundCounter(object sender, SSTransmitEventArgs args)
{
GetCurrentStrategy()?.UpdateLastWrite(args.server);
Interlocked.Add(ref _outboundCounter, args.length);
}
Source: [ShadowsocksController.cs – UpdateInboundCounter/UpdateOutboundCounter (lines 51‑61)](https://github.com/shadowsocks/shadowsocks-windows/blob/v4/shadowsocks-csharp/Controller/ShadowsocksController.cs#L51-L61)
Computing Per-Second Traffic Deltas
Once raw counting is active, a background thread computes per-second statistics by comparing counter snapshots and storing the differences in a circular buffer.
The TrafficStatistics Background Thread
When the controller initializes, it launches the statistics thread via StartTrafficStatistics(61)—the parameter specifying a queue size of 61 seconds:
StartTrafficStatistics(61); // ShadowsocksController.cs L1000‑L1004
The TrafficStatistics method runs an infinite loop that executes every 1000ms:
while (true)
{
var previous = trafficPerSecondQueue.Last();
var current = new TrafficPerSecond
{
inboundCounter = InboundCounter,
outboundCounter = OutboundCounter
};
current.inboundIncreasement = current.inboundCounter - previous.inboundCounter;
current.outboundIncreasement = current.outboundCounter - previous.outboundCounter;
trafficPerSecondQueue.Enqueue(current);
if (trafficPerSecondQueue.Count > queueMaxSize)
trafficPerSecondQueue.Dequeue();
TrafficChanged?.Invoke(this, EventArgs.Empty);
Thread.Sleep(1000);
}
Each iteration creates a TrafficPerSecond object containing both the absolute counters and the incremental delta (inboundIncreasement, outboundIncreasement). The queue maintains exactly 61 entries, providing a rolling one-minute window of historical data.
Source: [ShadowsocksController.cs – TrafficStatistics (lines 38‑57)](https://github.com/shadowsocks/shadowsocks-windows/blob/v4/shadowsocks-csharp/Controller/ShadowsocksController.cs#L38-L57)
Rendering Traffic Data in the Log Viewer UI
The Log Viewer window (LogForm) subscribes to the TrafficChanged event and transforms the raw deltas into visual chart data with automatic scaling.
Subscribing to TrafficChanged Events
LogForm registers its handler during initialization:
controller.TrafficChanged += controller_TrafficChanged; // LogForm.cs L74‑L76
The handler maintains its own trafficInfoQueue as a sliding window of recent deltas. On the first event, it seeds the queue with historical data from the controller's trafficPerSecondQueue; subsequently, it appends only the newest value:
private void controller_TrafficChanged(object sender, EventArgs e)
{
lock (_lock)
{
if (trafficInfoQueue.Count == 0)
{
// Initialize with zeros then backfill history
for (int i = 0; i < queueMaxLength; i++)
trafficInfoQueue.Enqueue(new TrafficInfo(0, 0));
foreach (var p in controller.trafficPerSecondQueue)
trafficInfoQueue.Enqueue(new TrafficInfo(p.inboundIncreasement,
p.outboundIncreasement));
}
else
{
var last = controller.trafficPerSecondQueue.Last();
trafficInfoQueue.Enqueue(new TrafficInfo(last.inboundIncreasement,
last.outboundIncreasement));
}
if (trafficInfoQueue.Count > queueMaxLength)
trafficInfoQueue.Dequeue();
}
}
Source: [LogForm.cs – controller_TrafficChanged (lines 32‑59)](https://github.com/shadowsocks/shadowsocks-windows/blob/v4/shadowsocks-csharp/View/LogForm.cs#L32-L59)
Chart Rendering and Scaling
A 100ms timer triggers UpdateTrafficChart, which performs four critical operations:
- Copies queue data into
List<float>collections for inbound and outbound points - Calculates the maximum speed to determine the Y-axis scale
- Applies
Utils.GetBandwidthScale()to convert bytes to human-readable units (KB/s, MB/s) - Binds data to the chart series and updates axis labels:
trafficChart.Series["Inbound"].Points.DataBindY(inboundPoints);
trafficChart.Series["Outbound"].Points.DataBindY(outboundPoints);
trafficChart.ChartAreas[0].AxisY.LabelStyle.Format = "{0:0.##} " + bandwidthScale.unitName;
trafficChart.ChartAreas[0].AxisY.Maximum = bandwidthScale.value;
// Update annotations with current values
inboundAnnotation.Text = Utils.FormatBandwidth(lastInbound);
outboundAnnotation.Text = Utils.FormatBandwidth(lastOutbound);
Source: [LogForm.cs – UpdateTrafficChart (lines 18‑30)](https://github.com/shadowsocks/shadowsocks-windows/blob/v4/shadowsocks-csharp/View/LogForm.cs#L18-L30)
Total Counters Display
The form title bar displays cumulative totals using Utils.FormatBytes:
this.Text = I18N.GetString("Log Viewer") +
$" [in: {Utils.FormatBytes(controller.InboundCounter)}, " +
$"out: {Utils.FormatBytes(controller.OutboundCounter)}]";
Source: [LogForm.cs – UpdateContent (lines 3‑4)](https://github.com/shadowsocks/shadowsocks-windows/blob/v4/shadowsocks-csharp/View/LogForm.cs#L3-L4)
Utility Functions for Bandwidth Formatting
The Utils class in shadowsocks-csharp/Util/Util.cs provides scaling logic that ensures chart axes adapt automatically to traffic fluctuations:
GetBandwidthScale(long maxSpeed): Returns aBandwidthScaleInfostruct containing the appropriate unit name (KiB/s, MiB/s, GiB/s), unit multiplier, and display limit based on the highest observed speedFormatBandwidth(long bytes): Converts per-second byte counts into formatted strings with dynamic unitsFormatBytes(long bytes): Used for cumulative counters in the title bar
Source: [Util.cs – Bandwidth formatting methods](https://github.com/shadowsocks/shadowsocks-windows/blob/v4/shadowsocks-csharp/Util/Util.cs)
Practical Code Examples
Subscribing to Traffic Updates in Custom Components
public class StatusMonitor
{
private readonly ShadowsocksController _controller;
public StatusMonitor(ShadowsocksController controller)
{
_controller = controller;
_controller.TrafficChanged += OnTrafficChanged;
}
private void OnTrafficChanged(object sender, EventArgs e)
{
long inbound = _controller.InboundCounter;
long outbound = _controller.OutboundCounter;
Console.WriteLine($"Total: ↓{Utils.FormatBytes(inbound)} ↑{Utils.FormatBytes(outbound)}");
}
}
Accessing Recent Per-Second Deltas
// Retrieve the most recent delta from the controller's queue
var latest = controller.trafficPerSecondQueue.Last();
Console.WriteLine($"Current speed: " +
$"↓{Utils.FormatBandwidth(latest.inboundIncreasement)}/s " +
$"↑{Utils.FormatBandwidth(latest.outboundIncreasement)}/s");
Implementing Custom Chart Updates
var timer = new System.Windows.Forms.Timer { Interval = 1000 };
timer.Tick += (_, __) =>
{
var recent = controller.trafficPerSecondQueue.Last();
// Add points to your chart series
chart.Series["Inbound"].Points.AddY(recent.inboundIncreasement);
chart.Series["Outbound"].Points.AddY(recent.outboundIncreasement);
// Maintain 60-point window
while (chart.Series["Inbound"].Points.Count > 60)
chart.Series["Inbound"].Points.RemoveAt(0);
};
timer.Start();
Summary
- Atomic counting:
ShadowsocksControllermaintains thread-safelongcounters updated viaInterlocked.AddwheneverTCPRelayorUDPRelayprocesses packets - Background aggregation: The
TrafficStatisticsthread computes per-second deltas every 1000ms, storing them in a fixed-size queue (default 61 entries) and raisingTrafficChangedevents - UI decoupling:
LogFormmaintains its own sliding window ofTrafficInfoobjects, ensuring the chart displays smooth data even if the UI thread momentarily lags - Dynamic scaling:
Utils.GetBandwidthScaleautomatically selects appropriate units (KiB/s, MiB/s) based on current traffic levels, updating both chart axes and text annotations - Dual display: The Log Viewer shows instantaneous per-second rates in the chart area while displaying cumulative totals in the window title
Frequently Asked Questions
How does Shadowsocks-Windows ensure thread safety when counting traffic bytes?
The application uses Interlocked.Add and Interlocked.Read operations on private long fields within ShadowsocksController. This lock-free approach allows multiple relay threads (TCPRelay and UDPRelay) to update counters simultaneously without blocking, while ensuring the UI thread reads consistent values when computing per-second deltas.
What is the default time window shown in the traffic chart?
The default window displays 61 seconds of historical data. The controller initializes trafficPerSecondQueue with this capacity in StartTrafficStatistics(61), and LogForm maintains a corresponding trafficInfoQueue of the same size, creating a rolling one-minute view of network activity.
Can I access traffic statistics programmatically from external plugins or extensions?
Yes. Any component with a reference to ShadowsocksController can subscribe to the public TrafficChanged event or read the InboundCounter and OutboundCounter properties directly. The controller also exposes the full trafficPerSecondQueue (containing TrafficPerSecond objects with delta values), allowing third-party code to access historical per-second data without modifying the source.
How is the bandwidth unit (KB/s vs MB/s) determined dynamically?
The Utils.GetBandwidthScale method examines the maximum speed value in the current data window and returns a scaling factor. If the highest value exceeds 1024 KiB/s, the scale switches to MiB/s; if it exceeds 1024 MiB/s, it switches to GiB/s. This BandwidthScaleInfo struct updates the chart's AxisY.Maximum and label format string every refresh cycle, ensuring the Y-axis always uses the most appropriate unit for the current traffic level.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →