webperf3.py - A web server to display iPerf3 results¶
This program consists of:
iPerf3 utilities, which run iPerf3 servers and interpret the results from JSON logs the servers create.
A webserver, which reports iPerf3 results.
A websocket and watcher, which looks for changes to the iPerf3 log files. A change causes the websocket to refresh the client, displaying any new results.
Possible extensions:
Provide a way to clear the table of results. This requires server-side code – simply write
{}
to the end of all log files, then refresh.Kill all the iperf3 servers when this program exits.
Table of Contents
Preamble¶
Imports¶
These are listed in the order prescribed by PEP 8.
Standard library¶
Third-party imports¶
Local application imports¶
Globals¶
The number of iPerf3 servers in use by this program.
The first port (which iPerf3 defaults to) to use when starting servers.
iPerf3 utilities¶
These functions interact with iPerf3.
Read iPerf3 logs¶
Read the last entry in a JSON-like log data from iPerf3, returning it as a Python data structure.
log_path: the Path (or its equivalent string) of the log file to read.
Returns the last iPerf3 result; this is a huge struct of iPerf3 data.
Each run of iPerf3 appends a valid JSON string to the log file; however, this makes the overall file invalid JSON after the first append. The structure looks like this:
1{
2 ...JSON data for execution i of iPerf3...
3}
4{
5 ...JSON data for execution i + 1 of iPerf3...
6}
Since we only care about the last run, a simple backwards search for a single line containing an {
with no leading spaces identifies the beginning of the last group of JSON data.
Special case: there’s only one block of JSON data; therefore, include the entire file when loading JSON data.
Errors produce confused JSON intermixed with error messages.
Read all entries in a JSON-like log data from iPerf3, returning them as an array of Python data structures.
See log_path.
Returns an array of iPerf3 results.
See comments in read_iperf3_json_log
.
Split the data based the beginning/end of JSON data.
Find the end of the current JSON block; if not found, go to the end of the string.
Interpret this chunk as JSON.
Move to the next chunk, or exit after the last chunk.
Extract iPerf3 data rates (bps) from its log data¶
The iPerf3 log data returned by read iPerf3 logs.
The timestamp of this performance measurement, in seconds sine the epoch.
The average bits per second sent by the server.
The average bits per second received by the server.
The extra data provided by the client, if present; None
otherwise.
Extract the relevant data from the JSON file.
timestamp = None
send_bps = None
receive_bps = None
try:
timestamp = iperf3_log_data["start"]["timestamp"]["timesecs"]
se = iperf3_log_data["end"]["streams"]
receive_bps = se[0]["receiver"]["bits_per_second"]
send_bps = se[1]["sender"]["bits_per_second"]
except (KeyError, IndexError):
pass
return timestamp, send_bps, receive_bps, iperf3_log_data.get("extra_data")
Name iPerf3 log files¶
A value between 0 and the num_servers
passed to start iPerf3 servers.
A Path to an iPerf3 log file.
Export all data¶
num_servers: The number of servers to read data from; must be a non-negative number.
num_servers: int,
) -> List[Tuple[int, Optional[int], Optional[float], Optional[float], Optional[str]]]:
iperf3_data = []
for server_index in range(num_servers):
log_file_name = iperf3_log_file_name(server_index)
log_data = read_all_iperf3_json_log(log_file_name)
port = server_index + starting_port
iperf3_data += [
(port,) + extract_iperf3_performance(json_log) for json_log in log_data
]
return iperf3_data
def export_csv(
See num_servers.
A string containing of the resulting CSV data.
Get the data to write; exclude blank entries. Indices in an element of data are:
Port
Timestamp
Send bps
Receive bps
Name
Sort by the timestamp, which is element 1 of each tuple in the list.
Convert the time to excel’s format, including moving from GMT to local time. Note that DATE(1970,1,1)
== 25569.
Write it out
Start iPerf3 servers¶
TODO: will all these subprocesses automatically be killed when this program exits? That’s what we want.
The number of servers to start; must be a non-negative number.
Webserver¶
Main page¶
This is the main web page which displays iPerf3 stats.
@route("/")
def home_page():
return dedent(
"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>iPerf3 performance measurements</title>
<!-- Use the ``ReconnectingWebsocket`` to automatically reconnect a websocket when the network connection drops. -->
<script src="/static/ReconnectingWebsocket.js?v=1"></script>
<script src="/static/webperf3.js?v=1"></script>
<style>
table, th, td {
border: 1px solid white;
border-collapse: collapse;
}
tr {
background-color: #96D4D4;
}
</style>
</head>
<body>
<h1>iPerf3 performance measurements</h1>
<table id="perf-table"></table>
<div>
Status: <span id="is_connected">waiting</span>.
Last update: <span id="last-update">Unknown</span>.
</div>
<br />
<div>
<button type="button" onclick="update_table();">Update now</button>
<button type="button" onclick="location.href='/csv'">Download all log data</button>
</div>
</body>
</html>
"""
)
Create the table of performance results.
Look at logs to get current iPerf3 data.
If there’s no log file yet, add a blank entry.
CSV download¶
See the bottle tutorial under “Changing the Default Encoding”.
See the Response object.
Static files¶
Serve static files (JS needed by the main page). Copied from the bottle docs.
Websocket and watcher¶
The watcher monitors the log directory, sending a message over a websocket to the client when the client needs to be updated.
A Path to the directory containing logs.
log_path: Path,
):
self.log_path = log_path
self.stop_event: Optional[asyncio.Event] = None
self.update_event: Optional[asyncio.Event] = None
self.thread = None
self.loop: Optional[asyncio.AbstractEventLoop] = None
def start(self):
self.thread = Thread(target=asyncio.run, args=(self.amain(),))
self.thread.start()
Shut down the websocket / watcher from another thread.
Websocket and watcher core¶
This handles an open websocket connection.
The opened websocket that can now be read or written.
Send the table when the page first loads, or after new data is available.
Wait until the watcher signals a change.
Look for shutdown.
Just allow the socket to close.
Very important: this hangs in shutdown unless we pass self.stop_event
to the watcher.
Signal any websockets to do an update.
Reset to prepare for the next signal.
On shutdown, signal any waiting websockets so they can exit.
This is the websocket server main loop, which waits for connections.
Run the watcher.
Start the server; per the docs, exiting this context manager shuts it down.
Run the server until a stop is requested.
Main¶
Parse command line.
Set up logging subdirectory.
Start the webserver and the watcher/websocket.
Ideally, run an asyncio server; however, I don’t understand how this would integrate into the event loop. So, use a multi-threaded server instead.
Shut down.