iperf3KMP is a cross-platform network performance testing app for Android, iOS, and desktop (macOS, Windows, Linux), built from a single Kotlin Multiplatform codebase with a shared Compose UI.
Highlights:
- Real iperf3, not a simulation — runs an embedded iperf3 3.21 client in-process (JNI on Android/desktop, Kotlin/Native cinterop on iOS) against the same shared C core on every platform.
- Genuine measurements — real TCP/UDP throughput, jitter, and loss, not canned or mocked output.
- Built-in history — results are saved locally via Room/SQLite, with a history view and CSV export.
- GUI and CLI — the desktop build ships both a Compose GUI and a standalone CLI tool.
- Native installers —
.dmg,.msi,.deb, and.rpmpackages for each desktop OS.
Documentation: see docs/README.md for an index of the architecture, per-platform build, and research docs.
Support: report bugs and ask questions in GitHub Issues. The maintainer address inside the Linux packages is a GitHub no-reply address, so email sent there isn't delivered.
- Android
arm64physical and simulatorx86_64simulator only
- iOS
arm64physical and simulatorx86_64simulator is not supported
- Desktop
GUI(JVM —macOS/Linux/Windows)
- macOS –
aarch64(M series only; nox86_64) - Linux –
x86_64
- rpm - Fedora/RHEL
- deb - Debian/Ubuntu
- rpm - Fedora/RHEL
- Linux –
arm64
- rpm - Fedora/RHEL
- deb - Debian/Ubuntu
- rpm - Fedora/RHEL
- Windows
x86_64arm64(partially x86_64 via Prism translation)
- macOS –
- Desktop
CLI(macOS/Linux/Windows)- Same platforms as
GUI
- Same platforms as
Note
curl -fsSL https://raw.githubusercontent.com/jerryabramson/iperf3-kmp/master/install.sh | bash
irm https://raw.githubusercontent.com/jerryabramson/iperf3-kmp/master/install.ps1 | iexWindows isn't distributed as a GitHub release asset — the script above just points you at the Microsoft Store listing (pending certification) or the platform build guide below.
Important
1.0.1 is out for iOS (TestFlight public beta) and Android (Google Play testing). The desktop installers further down are still 1.0.0; desktop 1.0.1 hasn't been released yet.
| Platform | Version | How to install |
|---|---|---|
| iOS / iPadOS | 1.0.1 | Open the Apple TestFlight link on the device (installs the TestFlight app first if needed). |
| Android | 1.0.1 | 1. Join the iperf3kmp-testers Google Group with the Google account your phone uses. 2. Open the Google Play test opt-in link on the phone, signed in with that account, accept, and install from Play. It can take a few minutes after joining before the link works ("item not found" until then). |
| Installer | Operating System | architecture |
|---|---|---|
| iperf3KMP-1.0.0.dmg | macOS | aarch64 |
| iperf3kmp_1.0.0_arm64.deb | Debian/Ubuntu | arm64 |
| iperf3kmp-1.0.0-1.aarch64.rpm | Fedora/RHEL | aarch64 |
| Windows ARM64 build guide — Store listing pending certification | Windows on ARM | arm64 |
| Installer | Operating System | architecture |
|---|---|---|
| Windows x86_64 build guide — Store listing pending certification | Windows | x86_64 |
| iperf3kmp_1.0.0_amd64.deb | Debian/Ubuntu | x86_64 |
| iperf3kmp-1.0.0-1.x86_64.rpm | Fedora/RHEL | x86_64 |
| artifact | Format |
|---|---|
| zip | zipfile |
| tar.gz | tarfile |
Tip
- Clone and build to a running app on the current platform/architecture:
git clone https://github.com/jerryabramson/iperf3-kmp --recursive cd iperf3-kmp/
- The full
esnet/iperfsource tree isn't included in this repo
it's a git submodule pinned to the
3.21tag, which is required for every target:
AndroidiOSandDesktopall link against itAfter cloning, if the
third_party/iperfsubdirectory is empty, run:git submodule update --init --recursive
- If the source code is downloaded as a zipfile or tar.gz
cd third_party rmdir iperf git clone --branch 3.21 --depth 1 https://github.com/esnet/iperf.git iperf
- Run desktop app (Linux, macOS, or Windows — on Windows use
gradlew.bat, see the Windows section below)
./gradlew :desktopApp:run
- /iosApp contains an iOS application.
- Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app.
- This is also where you should add SwiftUI code for your project.
- /androidApp is the Android application shell.
- MainActivity.kt is the entry point — a thin host for the shared Compose UI.
- /desktopApp is the JVM desktop application shell (macOS / Linux / Windows).
- main.kt is the entry point — it opens the Compose GUI by default, or dispatches to the cli package's
CliRunnerinstead if command-line arguments are passed. - Desktop packaging (
.dmg/.msi/.deb/.rpm) is configured in build.gradle.kts.
- main.kt is the entry point — it opens the Compose GUI by default, or dispatches to the cli package's
- /shared is for code that will be shared across your Compose Multiplatform applications.
- commonMain is for code that’s common for all targets.
- Platform-specific subfolders — compiled only for the platform named in the folder:
- androidMain — Android specific code.
- jvmMain — Desktop (JVM) specific code.
- iosMain — iOS specific code (Kotlin).
- For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, this folder would be the right place for such calls.
Tip
Use the run configurations provided by the run widget in your IDE's toolbar
Warning
Gradle command-line syntax and options
- Short per-platform build checklists (prerequisites, commands, gotchas) are in PLATFORM_BUILD_NOTES.md.
Needs the Android SDK (sdk.dir in local.properties, or ANDROID_HOME). To build only:
./gradlew :androidApp:assembleDebugTo also boot an emulator and run the app on it, entirely from the command line.
- To see the available AVDs:
$ANDROID_HOME/emulator/emulator -list-avds
- swap
Pixel_8for one of your own)
$ANDROID_HOME/emulator/emulator -avd Pixel_8 & # boots with a visible window
$ANDROID_HOME/platform-tools/adb wait-for-device
until [ "$($ANDROID_HOME/platform-tools/adb shell getprop sys.boot_completed | tr -d '\r')" = "1" ]; do sleep 2; done
./gradlew :androidApp:installDebug # builds + installs to the running emulator
$ANDROID_HOME/platform-tools/adb shell am start \
-n com.jabramson.iperf3_network_tester_kmp/.MainActivity- macOS + Xcode only — build/run is managed by Xcode's own tooling, not Gradle.
- Gradle's Kotlin Multiplatform plugin only builds the shared
Kotlin framework, not the final signed
.app). - Easiest is opening iosApp/iosApp.xcodeproj in Xcode and
running from there
- The application can also be built and launched on a Simulator entirely from the command line.
- no Xcode UI required.
- Simulator builds need no Apple account. To run on your own iPhone/iPad, put your Team ID and a bundle-ID prefix in a
gitignored
iosApp/Configuration/LocalSigning.xcconfig(copyLocalSigning.xcconfig.template) — see docs/platform-builds/ios.md.
# opens the Simulator window first, so it's visible throughout
open -a Simulator
# skip if a simulator is already booted
xcrun simctl boot "iPhone 17"
# Build and install
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator -configuration Debug \
-derivedDataPath build/ios build
xcrun simctl install booted build/ios/Build/Products/Debug-iphonesimulator/iperf3KMP.app
xcrun simctl launch booted com.jabramson.iperf3-network-tester-kmpTip
simctl boot alone starts the device headlessly with no visible window — open -a Simulator first is what actually
puts it on screen.
Requires a clang on PATH in order to compile the native iperf3/printf-formatting JNI bridges at build time; see CLAUDE.md's "Native iperf3 client" section):
# Normal build/run
gradlew :desktopApp:run # hot reload during development
gradlew :desktopApp:hotRun --auto - Windows is now supported.
- However, it has numerous special requirements and limitations
- Architectural Difference: Does not reuse the in-process JNI path the other desktop targets use
- instead, a bundled, Gradle-built-from-source
iperf3.exeruns as a real subprocess (ProcessBuilder).
- However, it has numerous special requirements and limitations
- Special Requirements:
- Cygwin installed at
C:\cygwin64with thegcc-coreandmakepackages- This builds the
iperf3.exeexecutable itself (Cygwin's owngcc, notclang;- Cygwin's bundled
clangcan't build against Cygwin's own newer headers.
- Cygwin's bundled
- This builds the
- A native Windows LLVM
clang(e.g.winget install LLVM.LLVM)- builds the JNI printf-formatting helper, same as macOS/Linux above.
- On Windows,
clangtakes its C headers and libraries from Visual Studio, so also install the Visual Studio Build Tools with the "Desktop development with C++" workload (add the ARM64 build tools on a Windows-on-Arm machine). - Without it the build fails with
'stdio.h' file not found.
- On Windows,
- builds the JNI printf-formatting helper, same as macOS/Linux above.
- The WiX Toolset — required by
jpackageto emit the.msi.
- Cygwin installed at
gradlew.bat :desktopApp:run
gradlew.bat :desktopApp:packageMsiFor the full story of why this was originally rejected and what changed. An MSI built on a Windows ARM64 machine has a native ARM64 JVM (built and installed on a
Parallels VM); the x64 MSI also runs there under Prism. Only iperf3.exe is x86_64 in both
(Cygwin has no ARM64 port); a native ARM64 iperf3.exe is planned but not started — see WINDOWS_ARM64_IPERF3_PLAN.md.
-
Pass any arguments and the desktop app runs a real iperf3 test from the terminal instead of opening the GUI — the exact same engine (real libiperf, live output parsing, stall detection), just rendered with ANSI escape codes instead of Compose widgets.
-
The packaged launcher (
iperf3KMP/iperf3KMP.bat/iperf3KMP.ps1) serves both uses; with Gradle, pass args via--args: -
Once the app is installed from a package (
.deb/.rpm/.dmg/.msi):bin/iperf3kmp(macOS/Linux)bin/iperf3kmp.ps1/bin/iperf3kmp.bat(Windows)
-
finds the installed launcher at its OS-specific location and run it, so you don't need to know jpackage's per-platform install layout:
# macOS/Linux
./bin/iperf3kmp my-iperf3-server.example.com -t 10 -P 4
# Windows Powershell
.\bin\iperf3kmp.ps1 my-iperf3-server.example.com -t 10 -P 4
# Windows BAT
.\bin\iperf3kmp.bat my-iperf3-server.example.com -t 10 -P 4 - See
desktopApp/.../cli/CliArgs.kt'susage()for the full flag list. - The progress track uses
▶/◀, which ordinary terminal fonts include. With a Nerd Font (e.g. Meslo) setIPERF3KMP_NERD_FONT=1(exportin your shell profile, orsetx IPERF3KMP_NERD_FONT 1on Windows) for the rounded Powerline caps instead. - ANSI output works out of the box on Windows too, and falls back to plain text (no escape codes) when
TERMlooks like a dumb terminal, so redirected-to-a-file and CI/log-capture output stays readable. - When checking how the live output actually looks (cursor positioning, in-place redraws, column alignment), test via the packaged binary (
bin/iperf3kmp/iperf3KMP.exe) or the built app directly — not./gradlew :desktopApp:run, even with--args/--console=plain:- Gradle wraps the forked JVM's stdout, so it isn't a real TTY from the process's own perspective.
- Buffering,
isatty()detection, and terminal-width queries can all behave differently than a genuine terminal. - This affects exactly what this mode requires: absolute cursor positioning and live in-place redraws.
| Platform | task |
|---|---|
| macOS | ./gradlew :desktopApp:packageDmg |
| macOS — local dev build, skips notarization (see important note below) | ./gradlew :desktopApp:packageDmgLocal |
| Debian/Ubuntu Linux x86_64 | ./gradlew :desktopApp:packageDeb |
| Fedora/Red Hat Linux x86_64 * | ./gradlew :desktopApp:packageRpm |
| Fedora/Red Hat aarch64 * | ./gradlew :desktopApp:packageRpm |
| Debian/Ubuntu Linux x86_64 or aarch64 - on a non-Debian host (Fedora/RHEL, or macOS with Docker Desktop) | scripts/packageDebianFromContainer.sh |
| Fedora/RHEL x86_64 or aarch64 - on a non-RPM host (Debian/Ubuntu, or macOS with Docker Desktop) | scripts/packageRpmFromContainer.sh |
| Windows x86_64 | gradlew.bat :desktopApp:packageMsi |
Windows ARM64 (native ARM64 JVM; bundled iperf3.exe is x86_64 under Prism emulation — see WINDOWS_ARM64_IPERF3_PLAN.md) |
gradlew.bat :desktopApp:packageMsi |
Important
packageDmgalways runs a realnotarytool submit --waitround trip to Apple's notarization service afterward (whendesktopApp.macNotarizationProfileis configured) — this can take a while, and Apple's notary service enforces its own rate limits, so runningpackageDmgrepeatedly in quick succession during local iteration risks hitting them. Use./gradlew :desktopApp:packageDmgLocalinstead for local testing: same signed.dmg, but it skips notarization/stapling entirely. Only use plainpackageDmgwhen you actually intend to distribute the result — apackageDmgLocalbuild isn't notarized, so Gatekeeper will flag it for anyone it's downloaded to.
Tip
Both container-based scripts auto-detect Podman or Docker (whichever is on
PATH) and build a native, non-emulated Linux container matching the host's CPU - including on Apple Silicon or Intel Macs via Docker Desktop, since it runs a real Linux VM matching the host architecture. Verified via Podman on Fedora x86_64 and Fedora aarch64, and via Docker Desktop on macOS (Apple Silicon) producing bothiperf3kmp_1.0.0_arm64.debandiperf3kmp-1.0.0-1.aarch64.rpm.
Caution
See Gotcha's in
CLAUDE.md
Tip
- Note: the
third_party/iperfsubmodule points at the publicesnet/iperfproject- Therefore, no authentication is required after cloning:
- Run
git submodule update --init --recursive

