Skip to content

perf(http): single Map probe in read_headers via update_or_default#390

Open
mizchi wants to merge 1 commit into
moonbitlang:mainfrom
mizchi:pr-http-update-or-default
Open

perf(http): single Map probe in read_headers via update_or_default#390
mizchi wants to merge 1 commit into
moonbitlang:mainfrom
mizchi:pr-http-update-or-default

Conversation

@mizchi

@mizchi mizchi commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Reader::read_headers in src/http/parser.mbt does two Map operations per header:

match headers.get(key) {
  None => headers[key] = value
  Some(value0) => headers[key] = "\{value0},\{value}"
}

Map::get walks the Robin-Hood probe sequence once; the subsequent headers[key] = ... walks it again from scratch. Map::update_or_default(key, default, update_fn) already exists for exactly this pattern and does a single probe.

headers.update_or_default(key, value, value0 => "\{value0},\{value}")

Why bother

Headers are parsed for every HTTP request. The save is small per call (one extra hash + one probe walk) but compounds across the request lifetime — and the new version is semantically identical and arguably clearer.

Scenario: bench-async/cmd/http_server_benchmark/main.mbt — the example HTTP server driven by k6 (3,330 req/s under valgrind, 67 kreq/s native). In the callgrind profile read_headers itself is 2.40% of instructions and the Map insert path inside it is a slice of that. Wall-time delta is within the run-to-run noise (~±2%) on a GET / workload — the visible work is dominated by moonbit_drop_object + malloc — but the patch costs nothing.

Test results

moonbitlang/async/http                  51 / 51 pass
moonbitlang/async                       87 / 87 pass
moonbitlang/async/aqueue                35 / 35 pass
moonbitlang/async/cond_var               5 /  5 pass
moonbitlang/async/semaphore              6 /  6 pass
moonbitlang/async/internal/coroutine     1 /  1 pass
moonbitlang/async/internal/event_loop    7 /  7 pass

Network round-trip tests (real socket / tls / http) require a network interface this sandbox lacks; this patch does not touch any code on that path.

Reader::read_headers did two Map operations per header:

  match headers.get(key) {
    None => headers[key] = value
    Some(value0) => headers[key] = "\\{value0},\\{value}"
  }

Map::get walks the Robin-Hood probe sequence once; the subsequent
headers[key] = ... walks it again. Map::update_or_default already
exists for this pattern and does a single probe.

Semantically identical (HTTP requires comma-joining duplicate header
names). Wall-time delta is within noise on GET / -- visible work is
dominated by moonbit_drop_object + malloc -- but the refactor costs
nothing and is arguably clearer.

@Guest0x0 Guest0x0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm except the minor comment issue

Comment thread src/http/parser.mbt
Some(value0) => headers[key] = "\{value0},\{value}"
}
// Single hashmap probe (was 2: get + set). For repeated header
// names HTTP requires comma-joining; for new keys we just insert.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the comment feels unnecessary here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants