# How Traffic Statistics Are Calculated, Tracked, and Displayed in Shadowsocks-Windows

> Learn how Shadowsocks-Windows calculates, tracks, and displays traffic statistics using atomic byte counters and background thread deltas for live UI charts.

- Repository: [shadowsocks/shadowsocks-windows](https://github.com/shadowsocks/shadowsocks-windows)
- Tags: internals
- Published: 2026-03-05

---

**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:

```csharp
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:

```csharp
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`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/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:

```csharp
StartTrafficStatistics(61);  // ShadowsocksController.cs L1000‑L1004

```

The `TrafficStatistics` method runs an infinite loop that executes every 1000ms:

```csharp
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`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/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:

```csharp
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:

```csharp
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`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/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:

1. **Copies** queue data into `List<float>` collections for inbound and outbound points
2. **Calculates** the maximum speed to determine the Y-axis scale
3. **Applies** `Utils.GetBandwidthScale()` to convert bytes to human-readable units (KB/s, MB/s)
4. **Binds** data to the chart series and updates axis labels:

```csharp
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`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/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`:

```csharp
this.Text = I18N.GetString("Log Viewer") + 
            $" [in: {Utils.FormatBytes(controller.InboundCounter)}, " +
            $"out: {Utils.FormatBytes(controller.OutboundCounter)}]";

```

*Source:* [[`LogForm.cs`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/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`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/shadowsocks-csharp/Util/Util.cs) provides scaling logic that ensures chart axes adapt automatically to traffic fluctuations:

- **`GetBandwidthScale(long maxSpeed)`**: Returns a `BandwidthScaleInfo` struct containing the appropriate unit name (KiB/s, MiB/s, GiB/s), unit multiplier, and display limit based on the highest observed speed
- **`FormatBandwidth(long bytes)`**: Converts per-second byte counts into formatted strings with dynamic units
- **`FormatBytes(long bytes)`**: Used for cumulative counters in the title bar

*Source:* [[`Util.cs`](https://github.com/shadowsocks/shadowsocks-windows/blob/main/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

```csharp
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

```csharp
// 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

```csharp
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**: `ShadowsocksController` maintains thread-safe `long` counters updated via `Interlocked.Add` whenever `TCPRelay` or `UDPRelay` processes packets
- **Background aggregation**: The `TrafficStatistics` thread computes per-second deltas every 1000ms, storing them in a fixed-size queue (default 61 entries) and raising `TrafficChanged` events
- **UI decoupling**: `LogForm` maintains its own sliding window of `TrafficInfo` objects, ensuring the chart displays smooth data even if the UI thread momentarily lags
- **Dynamic scaling**: `Utils.GetBandwidthScale` automatically 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.