Limiting a transfer rate between two devices to a specific value is relatively easy to implement. It requires a bit of math, std::chrono, and breaking one large write call into smaller pieces.


From previous code, I know that 14 to 20 nanoseconds is the expected smallest consecutive timestamps I can expect to see from Linux on a relatively new 3.4 GHz CPU. And with further testing I've performed since then on Windows using Microsoft Visual Studio, the std::chrono timestamps are actually much coarser.

Knowing this, I decided to use 1 millisecond time durations to control bandwidth. This means 1000 possible corrections per second, which also helps reduce loading the early part of each second. All the math is done using nanoseconds, but the eventual goal is 1 millisecond time slices.


There are 2 configuration values needed before we run the math:

  1. payload size; for this example, use 1200 byte buffers
  2. bandwidth in bps; for this example, use 10 Mbps

The trick is to figure out how many payload-sized writes to perform in each 1 millisecond time slice. Once the number of buffers have been written, we then sleep until the start of the next time slice.

I also briefly toyed with sleeping for a smaller equal number of nanoseconds between each written buffer, but this introduced too much overhead to be usable at high speeds.

If the bandwidth is high enough, or the payload size is small enough, then many writes may be necessary per time slice.

The following code figures out the best length of each time slice, and scales it up when necessary to be greater than or equal to 1 millisecond:

const size_t payload_size = 1200; const uint64_t bits_per_second = 10000000; // 10 Mbps const uint64_t bytes_per_second = bits_per_second / 8; uint64_t time_slice_in_ns = 1000000000; // 1 second == 1,000,000,000 ns time_slice_in_ns *= payload_size; time_slice_in_ns /= bytes_per_second; // if the time slice is too short (less than 1 millisecond) we need to write multiple buffers uint64_t factor = 1; if ( time_slice_in_ns < 1000000 ) // 1,000,000 nanoseconds == 1 millisecond { // figure out a multiplication factor to use to get us closer to 1 millisecond factor = std::ceil( 1000000.0 / time_slice_in_ns ); time_slice_in_ns *= factor; } const size_t number_of_buffers_to_write = factor; std::cout << "Need to write " << number_of_buffers_to_write << " " << payload_size << "-byte buffer(s) every " << time_slice_in_ns << " nanoseconds to achieve " << bits_per_second << " bps" << std::endl;

By the end of this code, time_slice_in_ns tells us the exact length of each time slice (should be approximately 1 millisecond), while number_of_buffers_to_write contains the exact number of payload-sized buffers which must be written during each time slice.


Several examples to show how this works:

1st example: Given a payload size of 1200 bytes and rate control of 10 Mbps:

2nd example: Given a payload size of 1000 bytes and rate control of 150 Mbps:

3rd example: Slower speeds will have a factor of 1. Given a payload size of 1234 bytes and rate control of 56 Kbps:

Writing the payload

Now that we know the amount of data to write and how frequently, the rest becomes quite simple. Pick a starting timestamp, write the required number of buffers, then sleep until the next timeslice begins. For example:

#include <chrono> #include <thread> ... // convert our time_slice_in_ns into a high resolution time duration const std::chrono::high_resolution_clock::duration length_of_time_slice = std::chrono::duration_cast(std::chrono::nanoseconds(time_slice_in_ns)); // calculate the exact time at which the next time slice is scheduled to start std::chrono::high_resolution_clock::time_point next_time_point = std::chrono::high_resolution_clock::now() + length_of_time_slice; size_t buffer_counter = 0; while ( not_done ) { buffer_counter ++; write_buffer( ... ); if ( buffer_counter >= number_of_buffers_to_write ) { buffer_counter = 0; const std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now(); if ( now < next_time_point ) { std::this_thread::sleep_until( next_time_point ); } next_time_point += length_of_time_slice; } }

Several things to note:

Last modified: 2018-06-05
Stéphane Charette, stephanecharette@gmail.com