Context
PR #75 added real loopback HTTP benchmarks for Oxy, package:http, and Dio.
The client comparison uses the same local HttpServer, the same payloads, and
real request/response consumption.
The new benchmark shows Oxy is competitive for larger transfer paths, but its
small-request fixed overhead is higher than package:http and Dio.
Current measurements
Temporary probe results from the same loopback-server shape, in microseconds per
run, median of samples:
| Scenario |
Oxy |
package:http |
Dio |
Notes |
| GET empty |
86.1 |
60.0 |
75.5 |
Oxy fixed overhead is visible. |
| GET JSON decode |
93.3 |
69.6 |
80.9 |
Oxy fixed overhead is visible. |
| GET 64KiB |
94.6 |
169.2 |
83.3 |
Oxy is faster than package:http, slightly slower than Dio. |
| POST JSON |
113.1 |
90.4 |
98.5 |
Oxy fixed overhead is visible. |
| POST 64KiB |
134.8 |
297.1 |
122.7 |
Oxy is faster than package:http, slightly slower than Dio. |
Approximate Oxy breakdown:
| Scenario |
Oxy total |
raw dart:io |
Oxy native transport wrapper |
Oxy client pipeline |
| GET empty |
86.1 |
52.9 / 61.5% |
11.6 / 13.5% |
21.5 / 25.0% |
| GET JSON decode |
93.3 |
63.7 / 68.2% |
15.9 / 17.0% |
13.8 / 14.8% |
| GET 64KiB |
94.6 |
70.0 / 74.0% |
20.0 / 21.2% |
4.6 / 4.9% |
| POST JSON |
113.1 |
76.8 / 67.9% |
13.8 / 12.2% |
22.5 / 19.9% |
| POST 64KiB |
134.8 |
74.7 / 55.4% |
34.2 / 25.4% |
25.8 / 19.2% |
Suspected hotspots
Client.send() always resolves options, merges hooks/middleware, creates a
lifecycle object, emits nullable events, and runs the full lifecycle even when
there are no hooks, middleware, timeouts, or custom policies.
_runOperation() creates a linked attempt signal for every attempt, even when
the parent signal is null. This likely pushes native transport into extra abort
handling paths.
- Successful responses appear to run status policy handling twice: once in
_runOperation() and again after the application pipeline returns.
- Native upload goes through
StreamIterator(body.stream()), abort handling,
timeout hooks, progress callbacks, flush, and cancel. This is useful for the
fully general path, but expensive for simple in-memory bodies without timeout,
abort, or progress callbacks.
- Oxy
Body is backed by ht.Body. ht.Body itself does not explain the small
request overhead, but Body.stream() is visible in the 64KiB upload path.
Scope for a future optimization PR
- Keep public API unchanged.
- Preserve timeout, retry, redirect, progress, cancellation, middleware, and
hook semantics.
- Add focused benchmarks before and after the optimization.
- Prefer small fast paths for the default no-middleware/no-hook/no-timeout
request path rather than a broad rewrite.
Candidate tasks
- Avoid creating a linked attempt signal when there is no parent signal or
timeout-driven internal signal.
- Remove duplicate success-path status policy application if it is confirmed
redundant.
- Add a no-op lifecycle fast path when middleware and hooks are empty.
- Add an in-memory body upload fast path in native transport when no abort,
timeout, or progress callbacks are active.
Acceptance criteria
dart analyze and dart test -p vm pass.
- Existing benchmark runner continues to work.
- Real loopback client benchmark improves Oxy small-request overhead without
regressing 64KiB transfer scenarios.
- Behavior tests cover any optimized paths that bypass the generic lifecycle.
Context
PR #75 added real loopback HTTP benchmarks for Oxy,
package:http, and Dio.The client comparison uses the same local
HttpServer, the same payloads, andreal request/response consumption.
The new benchmark shows Oxy is competitive for larger transfer paths, but its
small-request fixed overhead is higher than
package:httpand Dio.Current measurements
Temporary probe results from the same loopback-server shape, in microseconds per
run, median of samples:
package:http, slightly slower than Dio.package:http, slightly slower than Dio.Approximate Oxy breakdown:
dart:ioSuspected hotspots
Client.send()always resolves options, merges hooks/middleware, creates alifecycle object, emits nullable events, and runs the full lifecycle even when
there are no hooks, middleware, timeouts, or custom policies.
_runOperation()creates a linked attempt signal for every attempt, even whenthe parent signal is null. This likely pushes native transport into extra abort
handling paths.
_runOperation()and again after the application pipeline returns.StreamIterator(body.stream()), abort handling,timeout hooks, progress callbacks, flush, and cancel. This is useful for the
fully general path, but expensive for simple in-memory bodies without timeout,
abort, or progress callbacks.
Bodyis backed byht.Body.ht.Bodyitself does not explain the smallrequest overhead, but
Body.stream()is visible in the 64KiB upload path.Scope for a future optimization PR
hook semantics.
request path rather than a broad rewrite.
Candidate tasks
timeout-driven internal signal.
redundant.
timeout, or progress callbacks are active.
Acceptance criteria
dart analyzeanddart test -p vmpass.regressing 64KiB transfer scenarios.