Skip to content

fix(speedtest): pin parallel stream count instead of following host CPU count - #261

Open
s0up4200 wants to merge 4 commits into
developfrom
fix/speedtest-stream-count
Open

fix(speedtest): pin parallel stream count instead of following host CPU count#261
s0up4200 wants to merge 4 commits into
developfrom
fix/speedtest-stream-count

Conversation

@s0up4200

@s0up4200 s0up4200 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

speedtest-go takes its parallel stream count from runtime.NumCPU(). The result therefore depended on the host, not on the line. Users with 4-core boxes reported speeds well below the Ookla app on the same connection. Upload suffered most. On a path where one TCP stream cannot fill the line, 4 streams measured 179 Mbps upload where 24 measured 995. On a fast, low-latency path the count made no difference: 4 and 24 streams both gave 875 Mbps upload, against 889 Mbps from iperf3. A pinned count of 16 recovers a lot on a poor path and changes nothing on a good one. It also costs almost no CPU — a container limited to one core still reached line rate with 24 streams.

Two smaller faults go with it. Ookla reports the same distance for every server in a city, so choosing "the nearest" resolved an arbitrary tie: the lucky server gave 710 Mbps and the unlucky one 587. The choice now goes to the lowest measured latency among the closest servers. The runner also shared one speedtest-go client across all tests with no lock, so a scheduled test that overlapped a manual one recorded 0 Mbps. Tests now queue. Stored history will show a step up after this change, mostly in upload, and mostly on hosts with fewer than eight cores.

Summary by CodeRabbit

  • Improvements

    • Speed tests now run more consistently with parallel connections and serialized concurrent requests.
    • Nearby test servers are selected using latency and distance, while avoiding unsuitable distant servers.
    • Test state is reset between speed tests for more reliable results.
    • Canceled tests now stop promptly when another test is already running.
  • Tests

    • Added coverage for server selection and cancellation scenarios.

…PU count

speedtest-go takes its parallel stream count from runtime.NumCPU(), so the
result depended on the host and not on the line. A 4-core box measured about
half the upload of a 16-core box on the same connection. Pin the count so
every host runs the same test.

Also break the frequent distance ties on measured latency, and serialise
tests so an overlapping scheduled and manual run stop corrupting the shared
speedtest-go client.
@s0up4200 s0up4200 added go Pull requests that update Go code bugfix labels Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a303d6a9-dfff-4b57-a082-f331973f028f

📥 Commits

Reviewing files that changed from the base of the PR and between 36b93f0 and ea8a035.

📒 Files selected for processing (1)
  • internal/speedtest/speedtest_net_test.go

📝 Walkthrough

Walkthrough

The speedtest runner now uses 16 connections, selects nearby servers by distance and latency, serializes test execution, and resets client state after each test. Tests cover cancellation and server selection.

Changes

Speedtest selection and lifecycle

Layer / File(s) Summary
Nearest-server selection and validation
internal/speedtest/speedtest_net.go, internal/speedtest/speedtest_net_test.go
The client uses 16 connections. Server selection prefers the nearest server and uses the lowest positive latency among servers within 5 km. Tests cover empty input, ties, failed pings, fallback behavior, and distant low-latency servers.
Serialized test execution and cleanup
internal/speedtest/speedtest_net.go, internal/speedtest/speedtest_net_test.go
The runner serializes tests with a channel, returns context.Canceled when cancellation occurs before execution, resets the client after each test, and removes explicit selected-server context cleanup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • autobrr/netronome#215: Both PRs modify RunTest and server lookup behavior in internal/speedtest/speedtest_net.go.

Poem

A rabbit finds a server near,
And checks its latency clear.
Sixteen streams wait their turn,
Canceled tests do not return.
The client resets when tests adjourn.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and rationale but omits the required Summary, Why, Testing, Screenshots, and Checklist sections. Reformat the description with all required template sections and include testing results plus checklist status.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: pinning the speedtest parallel stream count instead of using host CPU count.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/speedtest-stream-count

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/speedtest/speedtest_net.go (1)

143-148: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return an error when no test server is selected.

selectNearestServer returns nil for an empty server list, and RunTest dereferences selectedServer when logging the server name. Guard selectedServer == nil after auto-selection and return an error instead of panicking. Add a RunTest regression case for an empty server list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/speedtest/speedtest_net.go` around lines 143 - 148, Update RunTest’s
auto-selection branch around selectNearestServer so it checks selectedServer
again after selecting from serverList and returns a descriptive error when no
server is available, before any dereference or logging. Add a RunTest regression
case covering an empty server list and asserting the error result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/speedtest/speedtest_net.go`:
- Line 55: Add a docs/ entry describing that the speedtest client’s
MaxConnections setting is fixed at 16 and explain how this fixed stream count
affects measurement results. Reference the speedtest configuration using the
existing MaxConnections setting and keep the documentation focused on this
behavior change.
- Around line 95-99: Update RunTest’s admission gate to wait on a context-aware
semaphore or equivalent instead of blocking on r.mu.Lock(); check ctx
cancellation before acquiring the gate and return immediately when cancelled.
Preserve serialized access to r.client, and ensure cancelled callers do not
reset or otherwise touch r.client.

---

Outside diff comments:
In `@internal/speedtest/speedtest_net.go`:
- Around line 143-148: Update RunTest’s auto-selection branch around
selectNearestServer so it checks selectedServer again after selecting from
serverList and returns a descriptive error when no server is available, before
any dereference or logging. Add a RunTest regression case covering an empty
server list and asserting the error result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 922142b7-8342-4336-bc50-4db63921027b

📥 Commits

Reviewing files that changed from the base of the PR and between a0c8176 and 84c431e.

📒 Files selected for processing (2)
  • internal/speedtest/speedtest_net.go
  • internal/speedtest/speedtest_net_test.go

Comment thread internal/speedtest/speedtest_net.go
Comment thread internal/speedtest/speedtest_net.go Outdated
Blocking on a plain mutex meant a request whose deadline expired while it
waited still started a full test once the lock came free. Queue on a
context-aware slot instead, and guard the empty server list that would
otherwise panic when logging the selected server.
@s0up4200

s0up4200 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — two of the three were real and are fixed in 71d1302.

Context-aware admission (major): correct, and it was introduced by this PR. The mutex made a request that had already timed out start a full test once the lock came free. RunTest now queues on a chan struct{} slot with a select on ctx.Done(), so a caller that has given up leaves without touching the client. Covered by TestRunTestGivesUpWhenCallerCancels.

Nil server (major): correct in shape. FetchServers() returns ErrServerNotFound for an empty list, so RunTest returns before selection and the path is unreachable today — but the guard is three lines and it prevents a panic, so it is in.

Docs entry (minor): declined. There is no configuration change here; the stream count is a fixed constant with no setting, so there is nothing for a user to set or read. docs/ currently holds one file and is not a reference for internals. The reasoning lives in the constant comment and the PR body, which is where a maintainer will look for it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/speedtest/speedtest_net_test.go`:
- Around line 20-30: Update RunTest to recheck ctx after acquiring the running
slot and before FetchServers, returning context.Canceled when cancellation
occurred while queued; add a regression test that queues behind the occupied
slot, cancels the context, releases the slot, and verifies the canceled error.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 31929e14-5654-438e-9e10-be47265cab51

📥 Commits

Reviewing files that changed from the base of the PR and between 84c431e and 71d1302.

📒 Files selected for processing (2)
  • internal/speedtest/speedtest_net.go
  • internal/speedtest/speedtest_net_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/speedtest/speedtest_net.go

Comment thread internal/speedtest/speedtest_net_test.go Outdated
When the slot frees, both select cases are ready at once and Go picks one at
random, so a caller that gave up while queued could still start a test half
the time. Recheck the context after the slot is taken, before touching the
client.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/speedtest/speedtest_net_test.go`:
- Around line 46-48: Replace the fixed time.Sleep in the RunTest test with a
deterministic synchronization hook or controlled slot-acquisition primitive that
signals once RunTest is blocked waiting on r.running; wait for that signal
before invoking cancel(), then release the slot and preserve the existing
assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8dc2ca3e-c580-4358-8c38-160cdc175962

📥 Commits

Reviewing files that changed from the base of the PR and between 71d1302 and 36b93f0.

📒 Files selected for processing (2)
  • internal/speedtest/speedtest_net.go
  • internal/speedtest/speedtest_net_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/speedtest/speedtest_net.go

Comment thread internal/speedtest/speedtest_net_test.go Outdated
The queued-caller test never reached the recheck. Cancelling before the slot
frees wakes the parked goroutine on ctx.Done() every time, so the branch under
test never ran and the test passed with the recheck removed. Cancel with the
slot free instead: both cases are then ready when the select is evaluated and
Go picks at random, so repeating it reaches the branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix go Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant