Skip to content

Commit 20425d7

Browse files
committed
Optimize the common header name handling
While the common header names and indices are defined in the capnp header, they are unlikely to change often (if at all). To avoid the overhead of building the hash map at runtime and performing the hash lookups, we can hardcode the common header names in an array and use direct indexing to retrieve them. Should at least save a handful of CPU cycles.
1 parent 3e29d21 commit 20425d7

File tree

1 file changed

+84
-77
lines changed

1 file changed

+84
-77
lines changed

src/workerd/api/headers.c++

Lines changed: 84 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,87 @@ void requireValidHeaderValue(kj::StringPtr value) {
9898
JSG_REQUIRE(c != '\0' && c != '\r' && c != '\n', TypeError, "Invalid header value.");
9999
}
100100
}
101+
102+
// If any more headers are added to the CommonHeaderName enum later, we should be careful about
103+
// introducing them into serialization. We need to roll out a change that recognizes the new IDs
104+
// before rolling out a change that sends them. MAX_COMMON_HEADER_ID is the max value we're willing
105+
// to send.
106+
static constexpr size_t MAX_COMMON_HEADER_ID =
107+
static_cast<size_t>(capnp::CommonHeaderName::WWW_AUTHENTICATE);
108+
109+
// Constexpr array of lowercase common header names (must match CommonHeaderName enum order
110+
// and must be kept in sync with the ordinal values defined in http-over-capnp.capnp). Since
111+
// it is extremely unlikely that those will change often, we hardcode them here for runtime
112+
// efficiency.
113+
static constexpr const char* COMMON_HEADER_NAMES[] = {
114+
nullptr, // 0: invalid
115+
"accept-charset", // 1
116+
"accept-encoding", // 2
117+
"accept-language", // 3
118+
"accept-ranges", // 4
119+
"accept", // 5
120+
"access-control-allow-origin", // 6
121+
"age", // 7
122+
"allow", // 8
123+
"authorization", // 9
124+
"cache-control", // 10
125+
"content-disposition", // 11
126+
"content-encoding", // 12
127+
"content-language", // 13
128+
"content-length", // 14
129+
"content-location", // 15
130+
"content-range", // 16
131+
"content-type", // 17
132+
"cookie", // 18
133+
"date", // 19
134+
"etag", // 20
135+
"expect", // 21
136+
"expires", // 22
137+
"from", // 23
138+
"host", // 24
139+
"if-match", // 25
140+
"if-modified-since", // 26
141+
"if-none-match", // 27
142+
"if-range", // 28
143+
"if-unmodified-since", // 29
144+
"last-modified", // 30
145+
"link", // 31
146+
"location", // 32
147+
"max-forwards", // 33
148+
"proxy-authenticate", // 34
149+
"proxy-authorization", // 35
150+
"range", // 36
151+
"referer", // 37
152+
"refresh", // 38
153+
"retry-after", // 39
154+
"server", // 40
155+
"set-cookie", // 41
156+
"strict-transport-security", // 42
157+
"transfer-encoding", // 43
158+
"user-agent", // 44
159+
"vary", // 45
160+
"via", // 46
161+
"www-authenticate", // 47
162+
};
163+
164+
kj::String getCommonHeaderName(uint id) {
165+
KJ_ASSERT(id > 0 && id <= MAX_COMMON_HEADER_ID, "Invalid common header ID");
166+
auto name = COMMON_HEADER_NAMES[id];
167+
KJ_DASSERT(name != nullptr);
168+
return kj::str(name);
169+
}
170+
171+
kj::Maybe<uint> getCommonHeaderId(kj::StringPtr name) {
172+
// It really shouldn't be possible for a name to be empty but just in case...
173+
if (name.size() == 0) return kj::none;
174+
for (uint i = 1; i <= MAX_COMMON_HEADER_ID; ++i) {
175+
KJ_DASSERT(COMMON_HEADER_NAMES[i] != nullptr);
176+
if (name == COMMON_HEADER_NAMES[i]) return i;
177+
}
178+
return kj::none;
179+
}
180+
181+
static_assert(std::size(COMMON_HEADER_NAMES) == (MAX_COMMON_HEADER_ID + 1));
101182
} // namespace
102183

103184
Headers::Headers(jsg::Lock& js, jsg::Dict<jsg::ByteString, jsg::ByteString> dict)
@@ -416,78 +497,6 @@ bool Headers::inspectImmutable() {
416497
// capitalization). So, it's certainly not worth it to try to keep the original capitalization
417498
// across serialization.
418499

419-
// If any more headers are added to the CommonHeaderName enum later, we should be careful about
420-
// introducing them into serialization. We need to roll out a change that recognizes the new IDs
421-
// before rolling out a change that sends them. MAX_COMMON_HEADER_ID is the max value we're willing
422-
// to send.
423-
static constexpr uint MAX_COMMON_HEADER_ID =
424-
static_cast<uint>(capnp::CommonHeaderName::WWW_AUTHENTICATE);
425-
426-
// ID for the `$commonText` annotation declared in http-over-capnp.capnp.
427-
// TODO(cleanup): Cap'n Proto should really codegen constants for annotation IDs so we don't have
428-
// to copy them.
429-
static constexpr uint64_t COMMON_TEXT_ANNOTATION_ID = 0x857745131db6fc83;
430-
431-
static kj::Array<kj::StringPtr> makeCommonHeaderList() {
432-
auto enums = capnp::Schema::from<capnp::CommonHeaderName>().getEnumerants();
433-
auto builder = kj::heapArrayBuilder<kj::StringPtr>(enums.size());
434-
bool first = true;
435-
for (auto e: enums) {
436-
if (first) {
437-
// Value zero is invalid, skip it.
438-
static_assert(static_cast<uint>(capnp::CommonHeaderName::INVALID) == 0);
439-
440-
// Add `nullptr` to the array so that our array indexes aren't off-by-one from the enum
441-
// values. We could in theory skip this and use +1 and -1 in a bunch of places but that seems
442-
// error-prone.
443-
builder.add(nullptr);
444-
445-
first = false;
446-
continue;
447-
}
448-
449-
kj::Maybe<kj::StringPtr> name;
450-
451-
// Look for $commonText annotation.
452-
for (auto ann: e.getProto().getAnnotations()) {
453-
if (ann.getId() == COMMON_TEXT_ANNOTATION_ID) {
454-
name = ann.getValue().getText();
455-
break;
456-
}
457-
}
458-
459-
builder.add(KJ_ASSERT_NONNULL(name));
460-
}
461-
462-
return builder.finish();
463-
}
464-
465-
static kj::ArrayPtr<const kj::StringPtr> getCommonHeaderList() {
466-
static const kj::Array<kj::StringPtr> LIST = makeCommonHeaderList();
467-
return LIST;
468-
}
469-
470-
static kj::HashMap<kj::String, uint> makeCommonHeaderMap() {
471-
kj::HashMap<kj::String, uint> result;
472-
auto list = getCommonHeaderList();
473-
KJ_ASSERT(MAX_COMMON_HEADER_ID < list.size());
474-
for (auto i: kj::range(1, MAX_COMMON_HEADER_ID + 1)) {
475-
auto key = kj::str(list[i]);
476-
for (auto& c: key) {
477-
if ('A' <= c && c <= 'Z') {
478-
c = c - 'A' + 'a';
479-
}
480-
}
481-
result.insert(kj::mv(key), i);
482-
}
483-
return result;
484-
}
485-
486-
static const kj::HashMap<kj::String, uint>& getCommonHeaderMap() {
487-
static const kj::HashMap<kj::String, uint> MAP = makeCommonHeaderMap();
488-
return MAP;
489-
}
490-
491500
void Headers::serialize(jsg::Lock& js, jsg::Serializer& serializer) {
492501
// We serialize as a series of key-value pairs. Each value is a length-delimited string. Each key
493502
// is a common header ID, or the value zero to indicate an uncommon header, which is then
@@ -503,10 +512,9 @@ void Headers::serialize(jsg::Lock& js, jsg::Serializer& serializer) {
503512
serializer.writeRawUint32(count);
504513

505514
// Now write key/values.
506-
auto& commonHeaders = getCommonHeaderMap();
507515
for (auto& entry: headers) {
508516
auto& header = entry.second;
509-
auto commonId = commonHeaders.find(header.key);
517+
auto commonId = getCommonHeaderId(header.key);
510518
for (auto& value: header.values) {
511519
KJ_IF_SOME(c, commonId) {
512520
serializer.writeRawUint32(c);
@@ -527,15 +535,14 @@ jsg::Ref<Headers> Headers::deserialize(
527535

528536
uint count = deserializer.readRawUint32();
529537

530-
auto commonHeaders = getCommonHeaderList();
531538
for (auto i KJ_UNUSED: kj::zeroTo(count)) {
532539
uint commonId = deserializer.readRawUint32();
533540
kj::String name;
534541
if (commonId == 0) {
535542
name = deserializer.readLengthDelimitedString();
536543
} else {
537-
KJ_ASSERT(commonId < commonHeaders.size());
538-
name = kj::str(commonHeaders[commonId]);
544+
KJ_ASSERT(commonId <= MAX_COMMON_HEADER_ID);
545+
name = getCommonHeaderName(commonId);
539546
}
540547

541548
auto value = deserializer.readLengthDelimitedString();

0 commit comments

Comments
 (0)