You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a --profile CLI switch with five profiles — Tiny, Small, Medium, Large, Mega — that select a coherent bundle of Ookla client settings (per-request sizes, iterations, parallelism, and total-byte budget caps) tailored to a target audience, ranging from IoT devices on 10 MB/month plans to inter-data-centre 10 Gbps fibre saturation. Medium becomes the new default. Profiles are exposed as a public NetPace.Core API so library/NuGet consumers benefit alongside the CLI.
Motivation
NetPace today exposes only two CLI knobs that influence payload size: --downloadsize and --uploadsize. Both are total-byte budget caps (default int.MaxValue), not per-request controls — see docs/architecture/download-upload-size-controls.md. The actual per-request sizes (DownloadSizes, DownloadSizeIterations, DownloadParallelTasks, UploadSizeIncrementKb, etc.) are hardcoded in OoklaSpeedtestSettings and reachable only from the library API.
Concrete consequences:
A user on a 10 MB/month IoT data plan cannot run NetPace at all without burning >30× their monthly allowance — defaults transfer ≈ 328 MiB down + ≈ 41 MiB up = ≈ 370 MiB per run.
A fibre/DC user wanting to saturate a 10 Gbps link cannot easily push parallelism above 8 or sustain transfer beyond ~370 MiB without dropping into the library API.
The two budget-cap flags do not protect against per-request fan-out — --downloadsize 1 still spawns 8 parallel HTTP GETs against full-sized JPEGs before the cap kicks in.
A single --profile knob that bundles all relevant settings collapses these concerns into one user-visible decision and makes NetPace usable across the full spectrum from IoT to inter-DC.
Proposal
Public Profile enum (NetPace.Core, provider-agnostic)
Profile is a domain-level vocabulary describing the intent of a test run (how much traffic, how aggressive). It carries no payload semantics on its own — it is just a set of labels. Each provider supplies its own translation of these labels into provider-specific settings. The enum lives at the top of NetPace.Core alongside the existing domain enums:
File path: src/NetPace.Core/Profile.cs — sibling of SpeedUnit, SpeedScale, SpeedUnitSystem. Mirrors their enum-backed CLI option pattern.
Profile → settings via constructor on OoklaSpeedtestSettings
Profile stays a pure provider-agnostic enum — no methods returning provider types, no awareness of any concrete provider.
OoklaSpeedtestSettings exposes two public constructors: a parameterless one that defaults to Profile.Medium, and a Profile-taking one that contains the entire profile → download/upload mapping inline as a single switch expression. There is no separate helper class.
publicsealedrecordOoklaSpeedtestSettings{publicServerDiscoverySettingsServerDiscovery{get;init;}=new();publicLatencyTestSettingsLatencyTest{get;init;}=new();publicDownloadTestSettingsDownloadTest{get;init;}publicUploadTestSettingsUploadTest{get;init;}// proxy settings unchanged/// <summary>Builds settings for the default profile (<see cref="Profile.Medium"/>).</summary>publicOoklaSpeedtestSettings():this(Profile.Medium){}/// <summary>Builds settings populated for the given profile.</summary>publicOoklaSpeedtestSettings(Profileprofile){(DownloadTest,UploadTest)=profileswitch{Profile.Tiny=>(newDownloadTestSettings{/* Tiny download */},newUploadTestSettings{/* Tiny upload */}),Profile.Small=>(newDownloadTestSettings{/* Small download */},newUploadTestSettings{/* Small upload */}),Profile.Medium=>(newDownloadTestSettings{/* Medium download */},newUploadTestSettings{/* Medium upload */}),Profile.Large=>(newDownloadTestSettings{/* Large download */},newUploadTestSettings{/* Large upload */}),Profile.Mega=>(newDownloadTestSettings{/* Mega download */},newUploadTestSettings{/* Mega upload */}),
_ =>thrownewArgumentOutOfRangeException(nameof(profile)),};}}
File path: src/NetPace.Core/Clients/Ookla/OoklaSpeedtestSettings.cs (existing file — gains the two constructors and the inline switch; DownloadTest / UploadTest lose their property initializers since the constructor now sets them).
Usage patterns:
// Default — Mediumvars=newOoklaSpeedtestSettings();// Explicit profilevars=newOoklaSpeedtestSettings(Profile.Tiny);// Profile + non-payload customisation (e.g. proxy)vars=newOoklaSpeedtestSettings(Profile.Mega)with{UseProxy=true,ProxyAddress=newUri("http://proxy.example.com:8080"),};// Profile + tweaked download cap (Mega but capped at 100 MiB instead of 1 GiB)varbaseSettings=newOoklaSpeedtestSettings(Profile.Mega);vars=baseSettingswith{DownloadTest=baseSettings.DownloadTestwith{DownloadSizeMb=100}};
Why this shape:
new OoklaSpeedtestSettings(profile) is the natural C# idiom — "construct one of these from this input". No factory-method indirection, no static-class call site, no helper file.
new OoklaSpeedtestSettings() defaults to Medium — matches CLI default behaviour (omit --profile → Medium) without any extra ceremony. Single source of truth: parameterless ctor chains via : this(Profile.Medium).
One file, one switch — every profile's concrete values are visible side-by-side in the constructor body. Easy to review, easy to test.
Profile enum stays pure — no extension methods on it return provider types; the dependency direction is correct (Ookla knows Profile; Profile knows nothing about Ookla).
Settings record instance state stays pure data — no Payload property, no profile field that could drift from the actual values.
Future provider — adds analogous constructors to its own settings record (e.g. MlabSpeedtestSettings(Profile)) with its own inline switch. Parallel structure; mapping is provider-specific.
with expressions work normally — the synthesised record copy constructor is unaffected by user-defined constructors.
The CLI consumes this by parsing --profile (default Profile.Medium), calling new OoklaSpeedtestSettings(parsedProfile), then applying any explicit-flag overrides via with expressions before passing to OoklaSpeedtest.
Per-test budget caps (DownloadSizeMb, UploadSizeMb) move into the settings records
The current --downloadsize / --uploadsize flags route to method parameters on GetDownloadSpeedAsync / GetUploadSpeedAsync (see docs/architecture/download-upload-size-controls.md §3). For profiles to drive these caps coherently, they should move into DownloadTestSettings.DownloadSizeMb and UploadTestSettings.UploadSizeMb. OoklaSpeedtest reads them from the settings record instead of taking them as separate arguments. The CLI flags still bind to the same final values; the data just flows via the settings record now.
Profile values (Ookla mapping)
Tiny, Small, Medium, and Large use only the historic Speedtest.net Flash-client array{350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000} to insulate ordinary users from any future OoklaServer change — see docs/architecture/download-upload-size-controls.md.
Mega is the deliberate exception: it includes the undocumented bonus payloads (5000, 6000, 7000) because they are the only way to reach steady-state on 10 Gbps fibre / inter-DC links. This is a power-user, opt-in tier — the brittleness is quarantined to one profile, called out in its XML doc, and flagged in the user guide.
Profile
DownloadSizes
DlIter
DlPar
DlCap (MiB)
UlIncKb
UlIncs
UlIter
UlPar
UlCap (MiB)
≈ Down per run
≈ Up per run
Target audience
Tiny
[350]
1
1
1
50
1
1
1
1
~245 KB
~50 KB
IoT, 10 MB/month plans (≤30 tests/month)
Small
[1000, 1500]
2
2
10
100
4
2
2
2
~10 MiB
~2 MiB
Mobile/cellular, metered
Medium
[1500, 2000, 3000, 3500, 4000]
2
4
100
200
6
5
4
25
~100 MiB
~21 MiB
Typical home broadband (default)
Large
[2000, 2500, 3000, 3500, 4000]
12
16
1024
500
8
12
16
256
~1 GiB
~211 MiB
Fibre, business
Mega
[3000, 4000, 5000, 6000, 7000]
40
32
10240
1024
16
16
32
2048
~10 GiB
~2 GiB
Inter-DC, 10 Gbps fibre saturation
Per-request bytes derive from the cross-server-validated table in docs/architecture/download-upload-size-controls.md. The download budget cap is set to actively truncate (rather than be inert at int.MaxValue), so per-run transfer is bounded even if the size×iteration product over-shoots.
These are Ookla-specific values. A second provider added later would supply its own translation of the same Profile enum into its own settings record.
CLI surface
Add to src/NetPace.Console/Program.cs following the established Option<TEnum> pattern (see the existing --unit-system definition):
varprofileOption=newOption<Profile>("--profile"){Description="Profile bundle of payload settings (Tiny | Small | Medium | Large | Mega).",DefaultValueFactory= _ =>Profile.Medium};
Wire into SpeedTestCommandSettings and use it as the seed for the OoklaSpeedtestSettings instance built in Program.RunAsync / SpeedTestCommand. Explicit --downloadsize / --uploadsize (and any future per-knob flags) override the profile's corresponding fields.
Default behaviour change
The default profile is Medium — this reduces per-run traffic from the current ≈ 370 MiB to ≈ 121 MiB. NetPace is pre-1.0, so this is treated as a routine change rather than a breaking-API event; the CHANGELOG entry will call it out.
Override interaction
The selected --profile is authoritative for the per-request shape (DownloadSizes, iterations, parallel tasks, upload increments). The only knobs an explicit CLI flag can override are the DownloadSizeMb / UploadSizeMb budget caps — those are always applied on top of the profile via with-expressions on the corresponding settings record.
Invocation
Builds settings via
netpace
new OoklaSpeedtestSettings() (chains to Profile.Medium)
netpace --profile tiny
new OoklaSpeedtestSettings(Profile.Tiny)
netpace --profile tiny --downloadsize 5
new OoklaSpeedtestSettings(Profile.Tiny) then with { DownloadTest = ... with { DownloadSizeMb = 5 } }
netpace --downloadsize 50
new OoklaSpeedtestSettings() then with { DownloadTest = ... with { DownloadSizeMb = 50 } }
When the override cap exceeds the profile's natural transfer total (e.g. --profile tiny --downloadsize 5000 — Tiny transfers ~245 KB; cap set to 5000 MiB), the override is mechanically present on the settings record but the cap-hit check (totalBytesReturned >= maxBytes inside GenericTestSpeedAsync) never triggers — the test completes naturally. The cap is a backstop, not a directive.
--no-download / --no-upload continue to short-circuit their respective phases regardless of profile.
Provider scope
The Profile enum is provider-agnostic and lives at the top of NetPace.Core (sibling of SpeedUnit, SpeedScale, SpeedUnitSystem). It is never extended with provider-specific knowledge — no extension method on Profile ever returns a provider type. Settings record instance state is also provider-pure — no Payload property, no profile in the data.
The provider-specific translation is entirely the provider's responsibility, contained in its settings record's Profile-taking constructor (e.g. OoklaSpeedtestSettings(Profile)) as a single inline switch expression. No companion helper classes.
When a second provider lands, it adds the analogous constructor to its own settings record (e.g. MlabSpeedtestSettings(Profile)) with its own inline switch. The mapping may not be byte-for-byte identical, and that's fine. No speculative abstraction now — just the right dependency direction (provider knows Profile; Profile knows no provider) to make adding a second provider cheap later.
Documentation
README.md — refresh the --help snapshot; add --profile to the options reference; add a one-line example (netpace --profile tiny).
USER_GUIDE.md — new "Choosing a profile" section with the budget table and decision guidance ("if you're on cellular, pick Small; if you're on fibre, pick Large; if you're saturating a 10 Gbps DC link, pick Mega"). Includes a dedicated warning callout for Mega explaining the undocumented-payload dependency.
docs/architecture/download-upload-size-controls.md — add a new section cross-referencing profiles to the per-request-size tables, so future maintainers can see how profile values were derived. Explicitly note that Mega is the only profile relying on the bonus 5000/6000/7000 payloads and document the fallback strategy if those payloads disappear upstream.
XML docs — required on Profile enum (and each enum member) and on both OoklaSpeedtestSettings constructors (per CLAUDE.md: all public APIs must have XML docs). The Profile.Mega enum member's XML doc must explicitly state: "Uses undocumented OoklaServer payloads (5000/6000/7000) which are not part of the historic Speedtest.net Flash-client array. May break on future OoklaServer releases — see docs/architecture/download-upload-size-controls.md."
CIR — new brief CIR documenting: (a) the public API addition (Profile enum, two new OoklaSpeedtestSettings constructors); (b) the rationale for putting Profile in NetPace.Core rather than the Console; (c) the deliberate dependency direction — provider's settings record gains a Profile-taking constructor with the entire profile→settings switch inline; Profile itself never references any provider; settings record instance state stays pure data with no profile field; no separate helper classes; (d) the move of DownloadSizeMb / UploadSizeMb from method parameters into DownloadTestSettings / UploadTestSettings to let profiles drive caps coherently.
CHANGELOG — entry noting the new flag, the new default, and the per-run traffic reduction.
Out of scope
Per-knob CLI flags exposing DownloadSizes, DownloadSizeIterations, DownloadParallelTasks, UploadSizeIncrementKb, UploadIncrements, UploadSizeIterations, UploadParallelTasks individually. Power users can keep using the library API; the goal of this issue is the curated-profile surface, not infinite knobs.
A user-defined / config-file-loaded custom profile. Out of scope; revisit if requested.
Generalising Profile across multiple providers. Single-provider today; abstract on second-provider arrival.
Auto-detection / adaptive profile selection ("pick a profile based on observed link speed"). Explicit user choice only.
Localisation of profile descriptions in --help output.
Acceptance criteria
Functional
Profile enum exists in NetPace.Core with values Tiny, Small, Medium, Large, Mega.
Profile enum lives at src/NetPace.Core/Profile.cs (top-level, not under Clients/Ookla/) — confirms its provider-agnostic nature.
Profile has no extension methods that reference any provider type — verified by grep / structural test.
OoklaSpeedtestSettings instance state has no Profile property — settings record state stays pure data.
OoklaSpeedtestSettings exposes two public constructors: OoklaSpeedtestSettings() and OoklaSpeedtestSettings(Profile profile).
The parameterless OoklaSpeedtestSettings() constructor chains to the Profile-taking one with Profile.Medium (single source of truth — : this(Profile.Medium)).
The Profile-taking constructor contains the entire profile→download/upload mapping inline as a single switch expression — there is no separate helper class, factory method, or extension method holding any of the per-profile values.
No OoklaSpeedtestSettingsExtensions, OoklaProfileExtensions, or any similar profile-related helper class exists in the codebase.
The constructor's switch expression throws ArgumentOutOfRangeException for unknown Profile values (defensive against future enum additions).
DownloadSizeMb and UploadSizeMb move from method parameters into DownloadTestSettings / UploadTestSettings respectively. OoklaSpeedtest reads them from the settings record.
--profile flag is parseable from the CLI, accepts the five values case-insensitively (matches existing enum-flag behaviour), and rejects anything else with a System.CommandLine error message.
Default profile when --profile is omitted is Medium.
Explicit --downloadsize / --uploadsize flags override the profile-derived DownloadSizeMb / UploadSizeMb (applied via with { DownloadTest = ... with { DownloadSizeMb = N } } after the factory); profile values for non-overridden settings are preserved.
--no-download / --no-upload continue to short-circuit their phases regardless of profile.
Per-run transferred byte totals fall within ±10 % of the targets in the profile table when run against a local Docker OoklaServer.
Tests (xUnit, mirroring the partial-class style of NetPaceConsoleTests.*)
Unit test per profile asserting the exact DownloadTest and UploadTest field values produced by new OoklaSpeedtestSettings(profile) (DownloadSizes, iterations, parallel tasks, increment Kb, caps).
Unit test that new OoklaSpeedtestSettings() produces field-for-field identical values to new OoklaSpeedtestSettings(Profile.Medium) (verifies the constructor-chaining wiring).
Unit test that Profile.Medium is returned when --profile is omitted.
Unit test that each profile name parses case-insensitively.
Unit test that --profile tiny --downloadsize 5 produces a settings record with the Tiny profile's DownloadSizes/iterations/parallel but DownloadTest.DownloadSizeMb == 5.
Unit test that --profile small --uploadsize 1 produces a settings record with the Small profile's upload increments but UploadTest.UploadSizeMb == 1.
Unit test that --no-download --profile large skips the download phase.
Unit test that new OoklaSpeedtestSettings(Profile.Mega).DownloadTest.DownloadSizes includes 5000, 6000, and 7000 (regression guard so a future refactor cannot silently demote Mega to historic-10 only).
Unit test that new OoklaSpeedtestSettings((Profile)999) throws ArgumentOutOfRangeException.
Optional integration test against the local Docker OoklaServer (docker/ooklaserver/) verifying Tiny actually transfers ≤ 1 MiB end-to-end.
Documentation
README.md --help snapshot updated; --profile documented in options table.
USER_GUIDE.md has a "Choosing a profile" section with the budget table and target-audience guidance.
All new public types/members in NetPace.Core carry XML docs.
New CIR added under docs/cir/ documenting the public API addition.
CHANGELOG entry added.
Open questions / future work
Mega fallback strategy if bonus payloads disappear upstream — if a future OoklaServer release stops serving 5000/6000/7000, fall back Mega.DownloadTest.DownloadSizes to historic-10 only (e.g. [2500, 3000, 3500, 4000]) with proportionally higher iteration counts to preserve the ~10 GiB target. A CI probe or release-time HEAD check (mirroring the cross-server validation already in docs/architecture/download-upload-size-controls.md) could detect this automatically.
Override-priority discoverability — should netpace --profile tiny --downloadsize 5 print a one-line "(profile X, with overrides: --downloadsize=5)" trace at Verbosity.Debug so users can confirm what actually applied? Likely yes, but folded into a follow-up.
Profile in output — should JSON/CSV output include the profile that was requested at the CLI (e.g. as a profile column/field) so logs can be filtered/grouped post-hoc? Since OoklaSpeedtestSettings doesn't carry the profile, the CLI would need to thread it through to the output writer separately. Useful but out of scope here.
Per-knob CLI flags — if real-world feedback shows the five profiles are too coarse, expose the underlying knobs in a follow-up. Don't pre-empt.
Confirmed decisions
CIR storage path: New CIR is filed under docs/change-intent-records/ (the existing repo directory and authoritative convention); the issue body's docs/cir/ reference is corrected before SDD.
Size-cap parameter overloads: Hard-remove the int downloadSizeMb / int uploadSizeMb parameter overloads from both OoklaSpeedtest and ISpeedTestService, leaving (server, ct) and (server, IProgress, ct) per direction; the cap is read from DownloadTestSettings.DownloadSizeMb / UploadTestSettings.UploadSizeMb on the settings record set at construction. Per-call variation uses settings with { DownloadSizeMb = N }. Accepted breaking change to the public NuGet contract; CHANGELOG breaking-change entry, XML doc updates on surviving methods, and call-site rewires at Program.cs:232-233 are in scope.
ISpeedTestService cap surface: Remove the existing int sizeMb overloads from ISpeedTestService.cs (GetDownloadSpeedAsync at L86 and L105; GetUploadSpeedAsync at L122 and L141) — "cap-free interface" means deletion, not just declining to add new ones. (Author redirected from "do not add an int downloadSizeMb overload to the interface" — corrected the framing: the overloads already exist on the interface and must be deleted.)
DownloadSizeMb / UploadSizeMb naming: Keep DownloadSizeMb / UploadSizeMb on the settings records (CLI flag spelling preserved); disambiguate from DownloadSizes (pixel array) via a one-line <remarks> on the XML doc.
Settings-record default value:int.MaxValue initializer on both DownloadSizeMb and UploadSizeMb, preserving "no cap unless explicitly set" semantics for raw-record consumers; profiles always overwrite, so the default never leaks into normal CLI/profile flow.
Docker integration test: No Docker-backed integration test for profile→bytes wiring; existing unit tests cover the profile→settings mapping. (Author redirected from "Tiny-only integration test against the local Docker OoklaServer" — Docker-backed integration tests considered an anti-pattern.)
Profile / override conflict: The selected --profile is authoritative for per-request shape (DownloadSizes, iterations, parallel tasks, upload increments). User-supplied --downloadsize / --uploadsize overrides always apply on top of --profile — they replace DownloadTest.DownloadSizeMb / UploadTest.UploadSizeMb on the settings record via with-application. When the override exceeds the profile's natural transfer total (e.g. --profile tiny --downloadsize 5000), the override is mechanically present on the record but the cap-hit check (totalBytesReturned >= maxBytes) never triggers because Tiny completes well below 5000 MiB on its own. The profile's per-request shape is strict; the cap acts as a backstop.
--profile flag aliases and error message: No short alias for --profile; rely on the default System.CommandLine error for unknown values.
Summary
Add a
--profileCLI switch with five profiles —Tiny,Small,Medium,Large,Mega— that select a coherent bundle of Ookla client settings (per-request sizes, iterations, parallelism, and total-byte budget caps) tailored to a target audience, ranging from IoT devices on 10 MB/month plans to inter-data-centre 10 Gbps fibre saturation.Mediumbecomes the new default. Profiles are exposed as a publicNetPace.CoreAPI so library/NuGet consumers benefit alongside the CLI.Motivation
NetPace today exposes only two CLI knobs that influence payload size:
--downloadsizeand--uploadsize. Both are total-byte budget caps (defaultint.MaxValue), not per-request controls — see docs/architecture/download-upload-size-controls.md. The actual per-request sizes (DownloadSizes,DownloadSizeIterations,DownloadParallelTasks,UploadSizeIncrementKb, etc.) are hardcoded inOoklaSpeedtestSettingsand reachable only from the library API.Concrete consequences:
--downloadsize 1still spawns 8 parallel HTTP GETs against full-sized JPEGs before the cap kicks in.A single
--profileknob that bundles all relevant settings collapses these concerns into one user-visible decision and makes NetPace usable across the full spectrum from IoT to inter-DC.Proposal
Public
Profileenum (NetPace.Core, provider-agnostic)Profileis a domain-level vocabulary describing the intent of a test run (how much traffic, how aggressive). It carries no payload semantics on its own — it is just a set of labels. Each provider supplies its own translation of these labels into provider-specific settings. The enum lives at the top ofNetPace.Corealongside the existing domain enums:File path:
src/NetPace.Core/Profile.cs— sibling ofSpeedUnit,SpeedScale,SpeedUnitSystem. Mirrors their enum-backed CLI option pattern.Profile → settings via constructor on
OoklaSpeedtestSettingsProfilestays a pure provider-agnostic enum — no methods returning provider types, no awareness of any concrete provider.OoklaSpeedtestSettingsexposes two public constructors: a parameterless one that defaults toProfile.Medium, and aProfile-taking one that contains the entire profile → download/upload mapping inline as a single switch expression. There is no separate helper class.File path:
src/NetPace.Core/Clients/Ookla/OoklaSpeedtestSettings.cs(existing file — gains the two constructors and the inline switch;DownloadTest/UploadTestlose their property initializers since the constructor now sets them).Usage patterns:
Why this shape:
new OoklaSpeedtestSettings(profile)is the natural C# idiom — "construct one of these from this input". No factory-method indirection, no static-class call site, no helper file.new OoklaSpeedtestSettings()defaults to Medium — matches CLI default behaviour (omit--profile→ Medium) without any extra ceremony. Single source of truth: parameterless ctor chains via: this(Profile.Medium).Profileenum stays pure — no extension methods on it return provider types; the dependency direction is correct (Ookla knowsProfile;Profileknows nothing about Ookla).Payloadproperty, no profile field that could drift from the actual values.MlabSpeedtestSettings(Profile)) with its own inline switch. Parallel structure; mapping is provider-specific.withexpressions work normally — the synthesised record copy constructor is unaffected by user-defined constructors.The CLI consumes this by parsing
--profile(defaultProfile.Medium), callingnew OoklaSpeedtestSettings(parsedProfile), then applying any explicit-flag overrides viawithexpressions before passing toOoklaSpeedtest.Per-test budget caps (
DownloadSizeMb,UploadSizeMb) move into the settings recordsThe current
--downloadsize/--uploadsizeflags route to method parameters onGetDownloadSpeedAsync/GetUploadSpeedAsync(see docs/architecture/download-upload-size-controls.md §3). For profiles to drive these caps coherently, they should move intoDownloadTestSettings.DownloadSizeMbandUploadTestSettings.UploadSizeMb.OoklaSpeedtestreads them from the settings record instead of taking them as separate arguments. The CLI flags still bind to the same final values; the data just flows via the settings record now.Profile values (Ookla mapping)
Tiny,Small,Medium, andLargeuse only the historic Speedtest.net Flash-client array{350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000}to insulate ordinary users from any future OoklaServer change — see docs/architecture/download-upload-size-controls.md.Megais the deliberate exception: it includes the undocumented bonus payloads (5000,6000,7000) because they are the only way to reach steady-state on 10 Gbps fibre / inter-DC links. This is a power-user, opt-in tier — the brittleness is quarantined to one profile, called out in its XML doc, and flagged in the user guide.[350][1000, 1500][1500, 2000, 3000, 3500, 4000][2000, 2500, 3000, 3500, 4000][3000, 4000, 5000, 6000, 7000]Per-request bytes derive from the cross-server-validated table in docs/architecture/download-upload-size-controls.md. The download budget cap is set to actively truncate (rather than be inert at
int.MaxValue), so per-run transfer is bounded even if the size×iteration product over-shoots.These are Ookla-specific values. A second provider added later would supply its own translation of the same
Profileenum into its own settings record.CLI surface
Add to src/NetPace.Console/Program.cs following the established
Option<TEnum>pattern (see the existing--unit-systemdefinition):Wire into
SpeedTestCommandSettingsand use it as the seed for theOoklaSpeedtestSettingsinstance built inProgram.RunAsync/SpeedTestCommand. Explicit--downloadsize/--uploadsize(and any future per-knob flags) override the profile's corresponding fields.Default behaviour change
The default profile is
Medium— this reduces per-run traffic from the current ≈ 370 MiB to ≈ 121 MiB. NetPace is pre-1.0, so this is treated as a routine change rather than a breaking-API event; the CHANGELOG entry will call it out.Override interaction
The selected
--profileis authoritative for the per-request shape (DownloadSizes, iterations, parallel tasks, upload increments). The only knobs an explicit CLI flag can override are theDownloadSizeMb/UploadSizeMbbudget caps — those are always applied on top of the profile viawith-expressions on the corresponding settings record.netpacenew OoklaSpeedtestSettings()(chains toProfile.Medium)netpace --profile tinynew OoklaSpeedtestSettings(Profile.Tiny)netpace --profile tiny --downloadsize 5new OoklaSpeedtestSettings(Profile.Tiny)thenwith { DownloadTest = ... with { DownloadSizeMb = 5 } }netpace --downloadsize 50new OoklaSpeedtestSettings()thenwith { DownloadTest = ... with { DownloadSizeMb = 50 } }When the override cap exceeds the profile's natural transfer total (e.g.
--profile tiny --downloadsize 5000— Tiny transfers ~245 KB; cap set to 5000 MiB), the override is mechanically present on the settings record but the cap-hit check (totalBytesReturned >= maxBytesinsideGenericTestSpeedAsync) never triggers — the test completes naturally. The cap is a backstop, not a directive.--no-download/--no-uploadcontinue to short-circuit their respective phases regardless of profile.Provider scope
The
Profileenum is provider-agnostic and lives at the top ofNetPace.Core(sibling ofSpeedUnit,SpeedScale,SpeedUnitSystem). It is never extended with provider-specific knowledge — no extension method onProfileever returns a provider type. Settings record instance state is also provider-pure — noPayloadproperty, no profile in the data.The provider-specific translation is entirely the provider's responsibility, contained in its settings record's
Profile-taking constructor (e.g.OoklaSpeedtestSettings(Profile)) as a single inline switch expression. No companion helper classes.When a second provider lands, it adds the analogous constructor to its own settings record (e.g.
MlabSpeedtestSettings(Profile)) with its own inline switch. The mapping may not be byte-for-byte identical, and that's fine. No speculative abstraction now — just the right dependency direction (provider knowsProfile;Profileknows no provider) to make adding a second provider cheap later.Documentation
--helpsnapshot; add--profileto the options reference; add a one-line example (netpace --profile tiny).Megaexplaining the undocumented-payload dependency.Megais the only profile relying on the bonus 5000/6000/7000 payloads and document the fallback strategy if those payloads disappear upstream.Profileenum (and each enum member) and on bothOoklaSpeedtestSettingsconstructors (per CLAUDE.md: all public APIs must have XML docs). TheProfile.Megaenum member's XML doc must explicitly state: "Uses undocumented OoklaServer payloads (5000/6000/7000) which are not part of the historic Speedtest.net Flash-client array. May break on future OoklaServer releases — see docs/architecture/download-upload-size-controls.md."Profileenum, two newOoklaSpeedtestSettingsconstructors); (b) the rationale for puttingProfileinNetPace.Corerather than the Console; (c) the deliberate dependency direction — provider's settings record gains aProfile-taking constructor with the entire profile→settings switch inline;Profileitself never references any provider; settings record instance state stays pure data with no profile field; no separate helper classes; (d) the move ofDownloadSizeMb/UploadSizeMbfrom method parameters intoDownloadTestSettings/UploadTestSettingsto let profiles drive caps coherently.Out of scope
DownloadSizes,DownloadSizeIterations,DownloadParallelTasks,UploadSizeIncrementKb,UploadIncrements,UploadSizeIterations,UploadParallelTasksindividually. Power users can keep using the library API; the goal of this issue is the curated-profile surface, not infinite knobs.Profileacross multiple providers. Single-provider today; abstract on second-provider arrival.--helpoutput.Acceptance criteria
Functional
Profileenum exists inNetPace.Corewith valuesTiny,Small,Medium,Large,Mega.Profileenum lives atsrc/NetPace.Core/Profile.cs(top-level, not underClients/Ookla/) — confirms its provider-agnostic nature.Profilehas no extension methods that reference any provider type — verified by grep / structural test.OoklaSpeedtestSettingsinstance state has noProfileproperty — settings record state stays pure data.OoklaSpeedtestSettingsexposes two public constructors:OoklaSpeedtestSettings()andOoklaSpeedtestSettings(Profile profile).OoklaSpeedtestSettings()constructor chains to theProfile-taking one withProfile.Medium(single source of truth —: this(Profile.Medium)).Profile-taking constructor contains the entire profile→download/upload mapping inline as a single switch expression — there is no separate helper class, factory method, or extension method holding any of the per-profile values.OoklaSpeedtestSettingsExtensions,OoklaProfileExtensions, or any similar profile-related helper class exists in the codebase.ArgumentOutOfRangeExceptionfor unknownProfilevalues (defensive against future enum additions).DownloadSizeMbandUploadSizeMbmove from method parameters intoDownloadTestSettings/UploadTestSettingsrespectively.OoklaSpeedtestreads them from the settings record.--profileflag is parseable from the CLI, accepts the five values case-insensitively (matches existing enum-flag behaviour), and rejects anything else with a System.CommandLine error message.--profileis omitted isMedium.--downloadsize/--uploadsizeflags override the profile-derivedDownloadSizeMb/UploadSizeMb(applied viawith { DownloadTest = ... with { DownloadSizeMb = N } }after the factory); profile values for non-overridden settings are preserved.--no-download/--no-uploadcontinue to short-circuit their phases regardless of profile.Tests (xUnit, mirroring the partial-class style of
NetPaceConsoleTests.*)DownloadTestandUploadTestfield values produced bynew OoklaSpeedtestSettings(profile)(DownloadSizes, iterations, parallel tasks, increment Kb, caps).new OoklaSpeedtestSettings()produces field-for-field identical values tonew OoklaSpeedtestSettings(Profile.Medium)(verifies the constructor-chaining wiring).Profile.Mediumis returned when--profileis omitted.--profile tiny --downloadsize 5produces a settings record with the Tiny profile'sDownloadSizes/iterations/parallel butDownloadTest.DownloadSizeMb == 5.--profile small --uploadsize 1produces a settings record with the Small profile's upload increments butUploadTest.UploadSizeMb == 1.--no-download --profile largeskips the download phase.new OoklaSpeedtestSettings(Profile.Mega).DownloadTest.DownloadSizesincludes5000,6000, and7000(regression guard so a future refactor cannot silently demote Mega to historic-10 only).new OoklaSpeedtestSettings((Profile)999)throwsArgumentOutOfRangeException.Documentation
--helpsnapshot updated;--profiledocumented in options table.NetPace.Corecarry XML docs.docs/cir/documenting the public API addition.Open questions / future work
Mega.DownloadTest.DownloadSizesto historic-10 only (e.g.[2500, 3000, 3500, 4000]) with proportionally higher iteration counts to preserve the ~10 GiB target. A CI probe or release-time HEAD check (mirroring the cross-server validation already in docs/architecture/download-upload-size-controls.md) could detect this automatically.netpace --profile tiny --downloadsize 5print a one-line "(profile X, with overrides: --downloadsize=5)" trace atVerbosity.Debugso users can confirm what actually applied? Likely yes, but folded into a follow-up.profilecolumn/field) so logs can be filtered/grouped post-hoc? SinceOoklaSpeedtestSettingsdoesn't carry the profile, the CLI would need to thread it through to the output writer separately. Useful but out of scope here.Confirmed decisions
docs/change-intent-records/(the existing repo directory and authoritative convention); the issue body'sdocs/cir/reference is corrected before SDD.int downloadSizeMb/int uploadSizeMbparameter overloads from bothOoklaSpeedtestandISpeedTestService, leaving(server, ct)and(server, IProgress, ct)per direction; the cap is read fromDownloadTestSettings.DownloadSizeMb/UploadTestSettings.UploadSizeMbon the settings record set at construction. Per-call variation usessettings with { DownloadSizeMb = N }. Accepted breaking change to the public NuGet contract; CHANGELOG breaking-change entry, XML doc updates on surviving methods, and call-site rewires at Program.cs:232-233 are in scope.ISpeedTestServicecap surface: Remove the existingint sizeMboverloads from ISpeedTestService.cs (GetDownloadSpeedAsyncat L86 and L105;GetUploadSpeedAsyncat L122 and L141) — "cap-free interface" means deletion, not just declining to add new ones. (Author redirected from "do not add anint downloadSizeMboverload to the interface" — corrected the framing: the overloads already exist on the interface and must be deleted.)DownloadSizeMb/UploadSizeMbnaming: KeepDownloadSizeMb/UploadSizeMbon the settings records (CLI flag spelling preserved); disambiguate fromDownloadSizes(pixel array) via a one-line<remarks>on the XML doc.int.MaxValueinitializer on bothDownloadSizeMbandUploadSizeMb, preserving "no cap unless explicitly set" semantics for raw-record consumers; profiles always overwrite, so the default never leaks into normal CLI/profile flow.--profileis authoritative for per-request shape (DownloadSizes, iterations, parallel tasks, upload increments). User-supplied--downloadsize/--uploadsizeoverrides always apply on top of--profile— they replaceDownloadTest.DownloadSizeMb/UploadTest.UploadSizeMbon the settings record viawith-application. When the override exceeds the profile's natural transfer total (e.g.--profile tiny --downloadsize 5000), the override is mechanically present on the record but the cap-hit check (totalBytesReturned >= maxBytes) never triggers because Tiny completes well below 5000 MiB on its own. The profile's per-request shape is strict; the cap acts as a backstop.--profileflag aliases and error message: No short alias for--profile; rely on the defaultSystem.CommandLineerror for unknown values.Related
OoklaSpeedtestSettingsshapes traffic; per-request byte tables; cross-server validation.Profileenum.