Skip to content

Commit 78080e5

Browse files
authored
Optimizations on hot path of the webserver - avoid splitting strings, copying arrays, excessive string equals etc. (helidon-io#10621)
1 parent 41a0c5c commit 78080e5

6 files changed

Lines changed: 52 additions & 21 deletions

File tree

common/parameters/src/main/java/io/helidon/common/parameters/Parameters.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ static Parameters create(String component, Map<String, List<String>> params) {
7474
return new ParametersMap(ParametersEmpty.MAPPERS, component, params);
7575
}
7676

77+
/**
78+
* Read only parameters based on a map.
79+
*
80+
* @param params underlying map
81+
* @param component component of the parameters to correctly report errors
82+
* @param qualifiers qualifiers to use when mapping to different types
83+
* @return new named parameters with values based on the map
84+
*/
85+
static Parameters create(String component, Map<String, List<String>> params, String... qualifiers) {
86+
// this method is to avoid parsing of component into qualifiers on WebServer hot path
87+
return new ParametersMap(ParametersEmpty.MAPPERS, component, params, qualifiers);
88+
}
89+
7790
/**
7891
* Read only parameters based on a map with just a single value.
7992
*

common/parameters/src/main/java/io/helidon/common/parameters/ParametersMap.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ class ParametersMap implements Parameters {
4141
this.params = new LinkedHashMap<>(params);
4242
}
4343

44+
ParametersMap(Mappers mapperManager, String component, Map<String, List<String>> params, String[] qualifiers) {
45+
this.qualifiers = qualifiers;
46+
this.mapperManager = mapperManager;
47+
this.component = component;
48+
this.params = new LinkedHashMap<>(params);
49+
}
50+
4451
@Override
4552
public List<String> all(String name) throws NoSuchElementException {
4653
List<String> value = params.get(name);

http/http/src/main/java/io/helidon/http/CookieParser.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,7 +51,9 @@ static Parameters parse(Header httpHeader) {
5151
if (allCookies.isEmpty()) {
5252
return EMPTY_COOKIES;
5353
}
54-
return Parameters.create("http/cookies", allCookies);
54+
// avoids splitting the component every single time parameters are created, which is
55+
// for example for every single HTTP request
56+
return Parameters.create("http/cookies", allCookies, "http", "cookies");
5557
}
5658

5759
static Parameters empty() {

http/http/src/main/java/io/helidon/http/HeaderValueCopy.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,7 +49,8 @@ public List<String> allValues() {
4949
@Override
5050
public int valueCount() {
5151
if (values == null) {
52-
values = new ArrayList<>(original.allValues());
52+
// no need to copy array just to geet a count
53+
return original.valueCount();
5354
}
5455
return values.size();
5556
}

http/http/src/main/java/io/helidon/http/PathMatchers.java

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ public static PathMatcher create(String pathPattern) {
162162
}
163163

164164
if (checkPattern.contains("{")
165-
|| checkPattern.contains("[")
166-
|| checkPattern.contains("*")
167-
|| checkPattern.contains("\\")) {
165+
|| checkPattern.contains("[")
166+
|| checkPattern.contains("*")
167+
|| checkPattern.contains("\\")) {
168168
return pattern(pathPattern);
169169
}
170170

@@ -285,13 +285,16 @@ private static String fixPrefix(String pathToMatch) {
285285
}
286286

287287
static final class ExactPathMatcher implements PathMatcher {
288+
private final boolean isRoot;
288289
private final String path;
289290
private final String pathWithTrailingSlash;
290291

291292
ExactPathMatcher(String path) {
292293
// We work with decoded URIs
293294
this.path = UriEncoding.decodeUri(path);
294295
this.pathWithTrailingSlash = this.path + "/";
296+
// optimization - this happens one per lifetime of the server, but the check is happening for every request
297+
this.isRoot = "/".equals(this.path);
295298
}
296299

297300
@Override
@@ -310,27 +313,29 @@ public MatchResult match(UriPath uriPath) {
310313

311314
@Override
312315
public PrefixMatchResult prefixMatch(UriPath uriPath) {
313-
if (path.equals("/")) {
316+
if (isRoot) {
314317
return new PrefixMatchResult(true,
315318
ROUTED_ROOT,
316319
uriPath);
317320
}
318321

319322
String actualPath = uriPath.path();
320-
if (actualPath.startsWith(pathWithTrailingSlash) || actualPath.equals(path)) {
323+
// first do an exact match, then a prefix match (to avoid doing prefix match if exactly matched)
324+
if (path.equals(actualPath)) {
325+
// this is an exact match
326+
return new PrefixMatchResult(true,
327+
new NoParamRoutedPath(uriPath),
328+
UriPath.createRelative(uriPath, "/"));
329+
}
330+
if (actualPath.startsWith(pathWithTrailingSlash)) {
321331
// we have /test
322332
String remaining = actualPath.substring(path.length());
323-
if (remaining.isEmpty()) {
324-
// we received /test
325-
return new PrefixMatchResult(true,
326-
new NoParamRoutedPath(uriPath),
327-
UriPath.createRelative(uriPath, "/"));
328-
} else {
329-
// we received /test/whatever, remaining should be /whatever
330-
return new PrefixMatchResult(true,
331-
new NoParamRoutedPath(UriPath.createRelative(uriPath, path)),
332-
UriPath.createRelative(uriPath, remaining));
333-
}
333+
334+
// we received /test/whatever, remaining should be /whatever
335+
return new PrefixMatchResult(true,
336+
new NoParamRoutedPath(UriPath.createRelative(uriPath, path)),
337+
UriPath.createRelative(uriPath, remaining));
338+
334339
}
335340
return PrefixMatchResult.notAccepted();
336341
}

webserver/webserver/src/main/java/io/helidon/webserver/http/RouteCrawler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ public CrawlerItem parent(RoutedPath parent) {
148148
newParams.put(paramName, params.all(paramName));
149149
}
150150
newParams.replaceAll((name, values) -> List.copyOf(values));
151-
RoutedPath result = new CrawlerRoutedPath(path, matchingElement, Parameters.create("http/path", newParams));
151+
// this is called for each request, optimize qualifiers so the do not get parsed each time
152+
RoutedPath result = new CrawlerRoutedPath(path,
153+
matchingElement,
154+
Parameters.create("http/path", newParams, "http", "path"));
152155
return new CrawlerItem(result, parent.path() + matchingElement, handler);
153156
}
154157

0 commit comments

Comments
 (0)