Skip to content

Commit 0b52f95

Browse files
authored
Optimize IngestDocument FieldPath allocation (#120573) (#121227)
1 parent 955ff93 commit 0b52f95

File tree

2 files changed

+47
-14
lines changed

2 files changed

+47
-14
lines changed

docs/changelog/120573.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 120573
2+
summary: Optimize `IngestDocument` `FieldPath` allocation
3+
area: Ingest Node
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/ingest/IngestDocument.java

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.common.Strings;
1313
import org.elasticsearch.common.util.CollectionUtils;
1414
import org.elasticsearch.common.util.Maps;
15+
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
1516
import org.elasticsearch.common.util.set.Sets;
1617
import org.elasticsearch.index.VersionType;
1718
import org.elasticsearch.index.mapper.IdFieldMapper;
@@ -189,8 +190,8 @@ public <T> T getFieldValue(String path, Class<T> clazz) {
189190
* or if the field that is found at the provided path is not of the expected type.
190191
*/
191192
public <T> T getFieldValue(String path, Class<T> clazz, boolean ignoreMissing) {
192-
FieldPath fieldPath = new FieldPath(path);
193-
Object context = fieldPath.initialContext;
193+
final FieldPath fieldPath = FieldPath.of(path);
194+
Object context = fieldPath.initialContext(this);
194195
for (String pathElement : fieldPath.pathElements) {
195196
ResolveResult result = resolve(pathElement, path, context);
196197
if (result.wasSuccessful) {
@@ -260,8 +261,8 @@ public boolean hasField(String path) {
260261
* @throws IllegalArgumentException if the path is null, empty or invalid.
261262
*/
262263
public boolean hasField(String path, boolean failOutOfRange) {
263-
FieldPath fieldPath = new FieldPath(path);
264-
Object context = fieldPath.initialContext;
264+
final FieldPath fieldPath = FieldPath.of(path);
265+
Object context = fieldPath.initialContext(this);
265266
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
266267
String pathElement = fieldPath.pathElements[i];
267268
if (context == null) {
@@ -328,8 +329,8 @@ public boolean hasField(String path, boolean failOutOfRange) {
328329
* @throws IllegalArgumentException if the path is null, empty, invalid or if the field doesn't exist.
329330
*/
330331
public void removeField(String path) {
331-
FieldPath fieldPath = new FieldPath(path);
332-
Object context = fieldPath.initialContext;
332+
final FieldPath fieldPath = FieldPath.of(path);
333+
Object context = fieldPath.initialContext(this);
333334
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
334335
ResolveResult result = resolve(fieldPath.pathElements[i], path, context);
335336
if (result.wasSuccessful) {
@@ -543,8 +544,8 @@ public void setFieldValue(String path, Object value, boolean ignoreEmptyValue) {
543544
}
544545

545546
private void setFieldValue(String path, Object value, boolean append, boolean allowDuplicates) {
546-
FieldPath fieldPath = new FieldPath(path);
547-
Object context = fieldPath.initialContext;
547+
final FieldPath fieldPath = FieldPath.of(path);
548+
Object context = fieldPath.initialContext(this);
548549
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
549550
String pathElement = fieldPath.pathElements[i];
550551
if (context == null) {
@@ -995,21 +996,45 @@ public String getFieldName() {
995996
}
996997
}
997998

998-
private class FieldPath {
999+
private static final class FieldPath {
9991000

1000-
private final String[] pathElements;
1001-
private final Object initialContext;
1001+
private static final int MAX_SIZE = 512;
1002+
private static final Map<String, FieldPath> CACHE = ConcurrentCollections.newConcurrentMapWithAggressiveConcurrency();
10021003

1003-
private FieldPath(String path) {
1004+
// constructing a new FieldPath requires that we parse a String (e.g. "foo.bar.baz") into an array
1005+
// of path elements (e.g. ["foo", "bar", "baz"]). Calling String#split results in the allocation
1006+
// of an ArrayList to hold the results, then a new String is created for each path element, and
1007+
// then finally a String[] is allocated to hold the actual result -- in addition to all that, we
1008+
// do some processing ourselves on the path and path elements to validate and prepare them.
1009+
// the above CACHE and the below 'FieldPath.of' method allow us to almost always avoid this work.
1010+
1011+
static FieldPath of(String path) {
10041012
if (Strings.isEmpty(path)) {
10051013
throw new IllegalArgumentException("path cannot be null nor empty");
10061014
}
1015+
FieldPath res = CACHE.get(path);
1016+
if (res != null) {
1017+
return res;
1018+
}
1019+
res = new FieldPath(path);
1020+
if (CACHE.size() > MAX_SIZE) {
1021+
CACHE.clear();
1022+
}
1023+
CACHE.put(path, res);
1024+
return res;
1025+
}
1026+
1027+
private final String[] pathElements;
1028+
private final boolean useIngestContext;
1029+
1030+
// you shouldn't call this directly, use the FieldPath.of method above instead!
1031+
private FieldPath(String path) {
10071032
String newPath;
10081033
if (path.startsWith(INGEST_KEY_PREFIX)) {
1009-
initialContext = ingestMetadata;
1034+
useIngestContext = true;
10101035
newPath = path.substring(INGEST_KEY_PREFIX.length());
10111036
} else {
1012-
initialContext = ctxMap;
1037+
useIngestContext = false;
10131038
if (path.startsWith(SOURCE_PREFIX)) {
10141039
newPath = path.substring(SOURCE_PREFIX.length());
10151040
} else {
@@ -1022,6 +1047,9 @@ private FieldPath(String path) {
10221047
}
10231048
}
10241049

1050+
public Object initialContext(IngestDocument document) {
1051+
return useIngestContext ? document.getIngestMetadata() : document.getCtxMap();
1052+
}
10251053
}
10261054

10271055
private static class ResolveResult {

0 commit comments

Comments
 (0)