PortLens is a native Windows app that tells you, in plain English, what every USB port, cable, charger and monitor on your machine is actually doing — and why something is slower than you expected.
You plug a 40 Gbps SSD into a 40 Gbps port with the cable that came in the box, and it copies at 5 Gbps. Device Manager says the device is working properly. Task Manager says the disk is barely busy. Nothing is broken, so nothing tells you anything.
macOS users have whatcable, which reads IOKit and answers
the question. Windows has had no equivalent. PortLens is that equivalent, and then some — because the
place Windows hurts most is not one cable, it is a dock with three monitors, eleven devices and one
shared bandwidth budget that nobody will show you.
Left 2 · USB-C (USB 3.2 Gen 2) 5 of 10 Gbps ⚠ Cable is the limit
Samsung T7 Shield SSD — supports 10 Gbps
████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░ 50%
⚠ The port does 40 Gbps and the drive reports 10 Gbps, but the link came up at
5 Gbps — that pattern points at the cable. [inferred]
Every connector on the machine, grouped and colour-coded by the box that owns it — this PC, the dock, the hub hiding behind the dock — showing what is plugged in, how fast it negotiated, and what it draws.
Windows reports the USB 2 and USB 3 halves of a single Type-C socket as two unrelated ports. PortLens puts them back together, then chases each device down the tree so a webcam three hops deep still says which socket on which box it ultimately hangs off.
Each port gets a verdict: negotiated speed versus what the device says it can do, with the bottleneck attributed to the cable, the port, the hub or the device. Real faults Windows reports — overcurrent, insufficient power, insufficient bandwidth, failed enumeration — are surfaced as facts, not buried in an error code.
Every monitor with its connection type, the physical socket it hangs off, its current mode against the maximum in its EDID, and an explanation of the gap. DisplayLink monitors — the ones that are not wired to your GPU at all — are traced back through the dock to the port the dock is plugged into, and labelled as compressed-over-USB so you stop blaming the graphics driver.
This is the part no other Windows tool does.
Pick a link rate, a cable mode and whether DSC is allowed, and PortLens computes the real DisplayPort MST budget hop by hop: pixel clock, bits per component, CVT-RB2 blanking, compression, the lane trade-off a USB-C port makes when it carries USB 3 alongside video.
When the chain does not fit, it does not just say so — it tells you exactly what to change, ranked:
| Remedy | Effect |
|---|---|
| Enable DSC | Visually lossless compression, roughly 3× |
| Drop to 8-bit colour | Frees ~25 % of the budget |
| Step the heaviest monitor down | 144 → 120 → 100 → 90 → 75 → 60 Hz |
| Switch the port to 4-lane DisplayPort | Doubles video, drops USB to 480 Mbps |
| Give a monitor its own port | Full link each, no compression needed |
Start from "Load my current setup" to model what you own, or change the pickers to model the dock you are about to order.
Real charge and discharge wattage, voltage, current, pack health, and a per-hub USB current budget grouped by the box each bus belongs to — so you can see the unpowered hub that is quietly rationing 500 mA between four devices.
Knowing a dock negotiated 5 Gbps is only half an answer. PortLens ships a database of docks, hubs, display
adapters and controllers, so it can compare what a device is doing against what it is for. Anything it
does not recognise is still reported honestly — and listed with its ID so you can teach PortLens about it
in your own devices.json.
Plug something in and PortLens rescans. If the device came up below its own capability, you get a notification while the cable is still in your hand — which is the only moment the information is actually useful.
Single click the tray icon for the flyout above: the sockets that need attention first, everything healthy and empty folded into one quiet line, then the displays — each row a shortcut into the matching page. Double click opens the window; right click gives the menu. Closing the window hides to the tray rather than exiting, because the point of the icon is to keep watching.
HTML or JSON covering every port, device, display, budget and verdict — for a support ticket, an RMA, or a forum thread where someone insists the cable cannot possibly be the problem. The CLI produces the same thing for scripts.
Record a fixture captures the whole machine to a file. portlens --replay recomputes everything from
that capture — which is how hardware you do not own gets debugged, and how the test suite runs with no
exotic hardware attached.
Windows does not expose USB Power Delivery contracts or cable e-marker data to user-mode applications. UCSI lives in the kernel and the OS surfaces only cooked results. There is no API that will tell you a cable is rated for 40 Gbps.
So PortLens infers what it can from what Windows does expose, and labels every derived conclusion
[inferred] and every direct reading [measured]. It will never invent a cable rating, and a blank
answer from the deep hardware probe is reported as a normal result rather than dressed up as a finding.
If you want a tool that confidently makes things up, this is not it.
dotnet build src\PortLens.App\PortLens.App.csproj -r win-x64
.\src\PortLens.App\bin\Debug\net10.0-windows10.0.19041.0\win-x64\PortLens.App.exeNo admin rights, no driver, no service, no telemetry. Pass --tray to start quietly in the notification
area.
Building on Arm64? Swap
win-x64forwin-arm64. Build the individual.csprojfiles rather than the solution — the SDK rejects a RID-specific build of the solution as a whole.
portlens --ports --displays --power # the same engine, in a terminal
portlens --chain # daisy-chain budget and remedies
portlens --devices # what the device database recognised
portlens --deep # what this machine will and will not tell us
portlens --html --out report.html # shareable report
portlens --record capture.json # capture this machine
portlens --replay capture.json # recompute everything from a capture.\tools\build-msix.ps1 # needs the Windows SDK; test-signs by defaultThe unpackaged build is the default, because a tool you reach for when a dock is misbehaving should be a folder you can copy onto a USB stick. The package exists for the Store, for winget, and for identity-backed toast notifications.
| Project | What lives there |
|---|---|
PortLens.Core |
Domain model, providers, inference rules, bandwidth maths, chain planner, device database, fixtures |
PortLens.App |
WinUI 3 shell: seven pages, tray icon, tray flyout, notifications |
PortLens.Cli |
Console front-end over the same engine |
PortLens.Tests |
xunit — 79 tests, all headless, all fixture-driven |
tools |
make-icon.ps1 draws the icon and tile assets; build-msix.ps1 packages |
docs |
ui-mockup.html — the clickable mockup the XAML was built from |
Interop is hand-written DllImport rather than CsWin32, deliberately: the USB IOCTL structs need
predictable layout, and two of them behave in ways a generator would not know about.
IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX_V2fails silently unless the caller sets theUsb300protocol bit on the input buffer.- The
Speedfield is unreliable on some xHCI drivers, so the EX_V2 capability flags win. - Dock firmware often fills in companion-port data for port 1 only, or not at all, so socket identity has to be recovered from the shape of the device tree.
- Physical port location comes from ACPI
_PLDand is not consistently surfaced, so the port map degrades to a logical grouping rather than inventing a chassis diagram it cannot verify.
The most valuable contribution is a fixture from hardware nobody here owns. Run
portlens --record capture.json, check that you are happy with what it contains, and open an issue with
it attached — Thunderbolt docks, UHBR monitors, USB4 enclosures and anything with a surprising topology
are all welcome.
Second most valuable: entries for devices.json, so more hardware gets compared against its real rating
instead of only being described.
MIT.
Inspired by whatcable by Darryl Morley, which answered this
question on macOS first.






