Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BitStream

A point-to-point network performance tester that lives in a single HTML file.

Download bitstream.html to two machines, open it on both, set one to Server and the other to Client, and press the big green button. No install, no runtime, no build step, no server infrastructure, and no internet access required.

The traffic travels over a WebRTC data channel — SCTP over DTLS over UDP — directly between the two machines. When both ends are on the same subnet there is no relay or proxy in the path, so the bytes genuinely traverse the link under test.


Quick start

  1. Copy bitstream.html to both machines and open it in a browser on each. Double-clicking the file is fine; file:// is a secure context, which is all WebRTC needs.
  2. Server machine: choose Server, press Generate session code, copy the code.
  3. Client machine: choose Client, paste that code, press Accept & generate reply, copy the reply code.
  4. Server machine: paste the reply, press Accept reply & connect. Both ends should show connected.
  5. Run the link check to get a latency baseline.
  6. On the client, set the options you want and press Apply configuration to both ends.
  7. Press START THROUGHPUT TEST.
  8. When it finishes, download the Markdown report. Either machine can export it — both ends receive the full result set.

The codes are long but you never type them: use the Copy button, or select-all and Ctrl+C. Each code displays a short checksum so you can confirm the paste arrived intact.


What it measures

Metric How
Throughput Application-layer goodput, measured at the receiving end
Latency Round-trip time of small probes over the control channel, reported as min/avg/max/mdev and p50/p95/p99, plus the browser's own ICE connectivity-check RTT
Jitter RFC 3550 smoothed mean deviation of transit time
Packet loss Per-packet sequence numbers against the sender's own message count (unreliable mode only)
Reordering / duplication Exact, over a 65 536-packet window
Payload integrity Received bytes verified against the transmitted bit pattern

Jitter is computed on the variation in transit time, so the unknown clock offset between the two machines cancels out. No clock synchronisation is needed, and none is assumed. The absolute one-way delay is not measurable this way and is deliberately not reported.

Packet loss uses the sender's own count as the denominator, shipped over the control channel when the stream ends — the receiver cannot otherwise know how many messages were meant to arrive. Per-interval loss is attributed against the sender's second-buckets, so a message delayed across a second boundary is charged to the interval it was sent in rather than the one it landed in.


Options

Option Notes
Transport mode Reliable/ordered (TCP-like) or unreliable/unordered (UDP-like). Loss, reordering and duplication only exist in unreliable mode; the reliable channel retransmits them away by design.
Direction Client→Server, Server→Client, or both simultaneously
Duration 3–300 seconds
Offered rate Unlimited, or a fixed Mbit/s enforced by a token-bucket pacer
Message size 1200–65536 bytes. This is the application message size, not the IP MTU.
Bit pattern PRBS-31, PRBS-9, random, all zeros, all ones, alternating 0x55/0xAA, or incrementing bytes
Integrity check Off, sampled (1 in 32), or every message

Fix the rate if you care about loss or jitter. At unlimited rate you are deliberately overrunning the path, so the loss figure tells you how far past capacity you pushed — not what the path's loss characteristics are. Set a fixed rate and the numbers become interpretable.

Bit patterns matter on links that compress. All-zeros will sail through a compressing link at a flattering rate. Use PRBS-31 or random for a fair measurement; use the compressible patterns deliberately when compression is what you are trying to detect.


Reading the throughput number honestly

A browser data channel costs CPU — SCTP framing, DTLS encryption, and JavaScript overhead all sit between you and the wire. On a fast link that cost, rather than the link, is usually what you hit. Expect a few hundred Mbit/s per direction; that is a browser limit, not a network one.

BitStream watches for this. It compares the achieved rate against the offered rate, tracks event-loop delay, and counts how often the transmit buffer filled. If the sending machine could not keep the channel fed, the result is labelled sender-limited, in the UI and in the exported report. A sender-limited result is a lower bound on the path's capacity, not a measurement of it.

Conversely, if the transmit buffer filled repeatedly, the transport was the constraint and the number means something.


What this tool does not do

Browsers have no access to raw sockets. Rather than fake these, BitStream leaves them out and says so in the interface and in every exported report:

  • Traceroute — needs ICMP and TTL control. Not possible in a browser, and not approximable.
  • ICMP ping — the latency figures are application-layer round trips and include a little browser scheduling overhead. Read them as an upper bound.
  • Port selection — ICE chooses ephemeral UDP ports.
  • QoS / DSCP marking — not exposed for data channels in any portable way.
  • Path MTU discovery — the size control sets an application message size. Messages larger than the path MTU are fragmented by SCTP, and in unreliable mode one lost fragment discards the whole message.

If you need any of these, use iperf3, mtr, or traceroute directly. BitStream's niche is that it needs nothing installed on either end.


Requirements and scope

  • Designed for two machines on the same subnet. No STUN or TURN server is involved, and the tool works with no internet access at all.
  • Chromium-first. Chrome and Edge give the richest getStats() data. Firefox and Safari work; fields they do not report show as "not reported" rather than breaking.
  • Both machines must run the same version of bitstream.html. The session code carries a protocol version and a mismatch is rejected with a clear message rather than failing obscurely.

Connecting across a router

Browsers hide local IP addresses behind randomised <uuid>.local mDNS hostnames. These resolve between machines on the same network segment but usually not across a router — which is the single most common reason a connection stalls at checking.

If that happens, open Advanced under Connect, enter a STUN server (stun:stun.l.google.com:19302 works if the machines have internet access), and exchange fresh codes. Note that DSCP-style prioritisation and same-segment assumptions no longer hold once traffic is routed, and if the connection ends up relayed through TURN the report will say so — those figures describe the path through the relay, not a direct path.


Verifying it works

Both checks run in a single browser tab, with no second machine and no network:

  • bitstream.html?selftest=1 — 122 unit assertions covering the jitter accumulator against hand-computed RFC 3550 values, PRBS generators against their known periods, the packet codec, the signalling codec, sequence/duplicate/reorder accounting, interval attribution, and Markdown generation against a fixture.
  • bitstream.html?loopback=1 — runs both peers inside the one page over real data channels, exercising the actual signalling codec and measurement path across reliable, unreliable, rate-limited, and bidirectional configurations.

The page title becomes PASS or FAIL, and results are also left on window.__bitstreamTestResult so the suites can be driven headlessly:

# mDNS candidate hiding must be off for a page to connect to itself headlessly
chromium --headless --disable-features=WebRtcHideLocalIpsWithMdns \
         'file:///path/to/bitstream.html?selftest=1'

Report output

Exported Markdown includes ISO-8601 timestamps with the local UTC offset, the full configuration, both ends' user agents, the selected ICE candidate pair, the link-check summary, per-second interval tables, summary statistics, the browser's own getStats() counters as a cross-check, a list of what was not measured, and the complete timestamped event log.

Warnings — sender-limited results, payload corruption, relayed paths — are placed at the top of the report, ahead of the numbers they qualify.


Landing page

site/ holds the landing page, published to bitstream.akhtar.app by .github/workflows/pages.yml. It drops onto any static host as-is. Its only third-party request is a Google Analytics tag; there are no CDNs, webfonts, or other external assets, and deleting that one script block from the <head> makes the page fully self-contained again.

bitstream.html itself carries no analytics and makes no outbound requests at all. That is a property worth protecting — the tool runs inside the networks people are troubleshooting — so do not add tracking to it. The Pages workflow copies the root bitstream.html over the site copy on every build, so anything added to site/bitstream.html would be overwritten anyway.

GitHub Pages can only serve a branch folder from / or /docs, so the site is uploaded as an Actions artifact instead. That is what lets it live in /site, and it also lets the build copy bitstream.html in from the repository root.

site/bitstream.html must stay byte-identical to the copy in the repository root: the app rejects a peer running a different protocol version, so a visitor who launches from the site has to get the same build as someone who downloaded it. The workflow fails the build if they diverge.

cp bitstream.html site/bitstream.html      # after any change to the app
sha256sum bitstream.html site/bitstream.html

To preview locally:

cd site && python3 -m http.server 8099     # then open http://localhost:8099

About

Push the bits. Measure the throughput.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages