Skip to content

perf: reduce client and native transport overhead #76

Description

@medz

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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions