Skip to content

Commit bb08d43

Browse files
l46kokcopybara-github
authored andcommitted
Optimize list/map adaptations
PiperOrigin-RevId: 905314230
1 parent f566450 commit bb08d43

16 files changed

Lines changed: 2268 additions & 34 deletions

File tree

common/internal/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,8 @@ cel_android_library(
147147
name = "date_time_helpers_android",
148148
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
149149
)
150+
151+
java_library(
152+
name = "reflection_util",
153+
exports = ["//common/src/main/java/dev/cel/common/internal:reflection_util"],
154+
)

common/src/main/java/dev/cel/common/internal/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,12 @@ java_library(
398398
java_library(
399399
name = "reflection_util",
400400
srcs = ["ReflectionUtil.java"],
401+
tags = [
402+
"alt_dep=//common/internal:reflection_util",
403+
],
401404
deps = [
402405
"//common/annotations",
406+
"@maven//:com_google_guava_guava",
403407
],
404408
)
405409

common/src/main/java/dev/cel/common/internal/ReflectionUtil.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@
1414

1515
package dev.cel.common.internal;
1616

17+
import com.google.common.reflect.TypeToken;
1718
import dev.cel.common.annotations.Internal;
1819
import java.lang.reflect.InvocationTargetException;
1920
import java.lang.reflect.Method;
21+
import java.lang.reflect.ParameterizedType;
22+
import java.lang.reflect.Type;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Optional;
2026

2127
/**
2228
* Utility class for invoking Java reflection.
@@ -48,5 +54,48 @@ public static Object invoke(Method method, Object object, Object... params) {
4854
}
4955
}
5056

57+
/**
58+
* Extracts the element type of a container type (List, Map, or Optional). Returns the type itself
59+
* if it's not a container or if generic type info is missing.
60+
*/
61+
public static Class<?> getElementType(Class<?> type, Type genericType) {
62+
TypeToken<?> token = TypeToken.of(genericType);
63+
64+
if (List.class.isAssignableFrom(type)) {
65+
return token.resolveType(List.class.getTypeParameters()[0]).getRawType();
66+
}
67+
if (Map.class.isAssignableFrom(type)) {
68+
return token.resolveType(Map.class.getTypeParameters()[1]).getRawType();
69+
}
70+
if (type == Optional.class) {
71+
return token.resolveType(Optional.class.getTypeParameters()[0]).getRawType();
72+
}
73+
74+
return type;
75+
}
76+
77+
/**
78+
* Extracts the raw Class from a Type. Handles Class, ParameterizedType, and WildcardType (returns
79+
* upper bound). Returns Object.class as fallback.
80+
*/
81+
public static Class<?> getRawType(Type type) {
82+
return TypeToken.of(type).getRawType();
83+
}
84+
85+
/**
86+
* Extracts the actual type arguments from a ParameterizedType, if it has at least the expected
87+
* minimum number of arguments. Returns Optional.empty() if the type is not parameterized or has
88+
* fewer arguments than expected.
89+
*/
90+
public static Optional<Type[]> getTypeArguments(Type type, int minArgs) {
91+
if (type instanceof ParameterizedType) {
92+
Type[] args = ((ParameterizedType) type).getActualTypeArguments();
93+
if (args.length >= minArgs) {
94+
return Optional.of(args);
95+
}
96+
}
97+
return Optional.empty();
98+
}
99+
51100
private ReflectionUtil() {}
52101
}

common/src/main/java/dev/cel/common/values/BUILD.bazel

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ java_library(
6060
deps = [
6161
"//common/values",
6262
"@maven//:com_google_errorprone_error_prone_annotations",
63-
"@maven//:com_google_guava_guava",
6463
],
6564
)
6665

@@ -72,7 +71,6 @@ cel_android_library(
7271
deps = [
7372
"//common/values:values_android",
7473
"@maven//:com_google_errorprone_error_prone_annotations",
75-
"@maven_android//:com_google_guava_guava",
7674
],
7775
)
7876

@@ -118,7 +116,6 @@ java_library(
118116
deps = [
119117
":values",
120118
"//common/annotations",
121-
"@maven//:com_google_errorprone_error_prone_annotations",
122119
"@maven//:com_google_guava_guava",
123120
"@maven//:org_jspecify_jspecify",
124121
],
@@ -134,12 +131,31 @@ cel_android_library(
134131
deps = [
135132
":values_android",
136133
"//common/annotations",
137-
"@maven//:com_google_errorprone_error_prone_annotations",
138134
"@maven//:org_jspecify_jspecify",
139135
"@maven_android//:com_google_guava_guava",
140136
],
141137
)
142138

139+
java_library(
140+
name = "preadapted_list",
141+
srcs = [
142+
"CelPreAdaptedList.java",
143+
],
144+
tags = [
145+
],
146+
deps = ["//common/annotations"],
147+
)
148+
149+
cel_android_library(
150+
name = "preadapted_list_android",
151+
srcs = [
152+
"CelPreAdaptedList.java",
153+
],
154+
tags = [
155+
],
156+
deps = ["//common/annotations"],
157+
)
158+
143159
java_library(
144160
name = "values",
145161
srcs = CEL_VALUES_SOURCES,
@@ -148,6 +164,7 @@ java_library(
148164
deps = [
149165
":cel_byte_string",
150166
":cel_value",
167+
":preadapted_list",
151168
"//:auto_value",
152169
"//common/annotations",
153170
"//common/types",
@@ -198,6 +215,7 @@ cel_android_library(
198215
deps = [
199216
":cel_byte_string",
200217
":cel_value_android",
218+
":preadapted_list_android",
201219
"//:auto_value",
202220
"//common/annotations",
203221
"//common/types:type_providers_android",
@@ -226,14 +244,14 @@ java_library(
226244
],
227245
deps = [
228246
":cel_byte_string",
229-
":values",
230247
"//common/annotations",
231248
"//common/internal:proto_time_utils",
232249
"//common/internal:well_known_proto",
233250
"//common/values",
234251
"@maven//:com_google_errorprone_error_prone_annotations",
235252
"@maven//:com_google_guava_guava",
236253
"@maven//:com_google_protobuf_protobuf_java",
254+
"@maven_android//:com_google_protobuf_protobuf_javalite",
237255
],
238256
)
239257

@@ -261,6 +279,7 @@ java_library(
261279
],
262280
deps = [
263281
":base_proto_cel_value_converter",
282+
":preadapted_list",
264283
":values",
265284
"//:auto_value",
266285
"//common:options",
@@ -273,7 +292,7 @@ java_library(
273292
"@maven//:com_google_errorprone_error_prone_annotations",
274293
"@maven//:com_google_guava_guava",
275294
"@maven//:com_google_protobuf_protobuf_java",
276-
"@maven//:org_jspecify_jspecify",
295+
"@maven_android//:com_google_protobuf_protobuf_javalite",
277296
],
278297
)
279298

@@ -316,8 +335,6 @@ java_library(
316335
"//protobuf:cel_lite_descriptor",
317336
"@maven//:com_google_errorprone_error_prone_annotations",
318337
"@maven//:com_google_guava_guava",
319-
"@maven//:com_google_protobuf_protobuf_java",
320-
"@maven//:org_jspecify_jspecify",
321338
"@maven_android//:com_google_protobuf_protobuf_javalite",
322339
],
323340
)
@@ -343,7 +360,6 @@ cel_android_library(
343360
"//protobuf:cel_lite_descriptor",
344361
"@maven//:com_google_errorprone_error_prone_annotations",
345362
"@maven//:com_google_guava_guava",
346-
"@maven//:org_jspecify_jspecify",
347363
"@maven_android//:com_google_guava_guava",
348364
"@maven_android//:com_google_protobuf_protobuf_javalite",
349365
],
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.common.values;
16+
17+
import dev.cel.common.annotations.Internal;
18+
import java.util.AbstractList;
19+
import java.util.List;
20+
import java.util.RandomAccess;
21+
22+
/**
23+
* A zero-allocation view over a list we know is already adapted.
24+
*
25+
* <p>This class purely exists as an optimization scheme to avoid redundant collection traversals
26+
* in {@link CelValueConverter}, and is not intended for general use.
27+
*/
28+
@Internal
29+
final class CelPreAdaptedList<E> extends AbstractList<E>
30+
implements RandomAccess {
31+
private final List<E> delegate;
32+
33+
private CelPreAdaptedList(List<E> delegate) {
34+
this.delegate = delegate;
35+
}
36+
37+
static <E> CelPreAdaptedList<E> wrap(List<E> safeList) {
38+
return new CelPreAdaptedList<>(safeList);
39+
}
40+
41+
@Override
42+
public E get(int index) {
43+
return delegate.get(index);
44+
}
45+
46+
@Override
47+
public int size() {
48+
return delegate.size();
49+
}
50+
}

common/src/main/java/dev/cel/common/values/CelValueConverter.java

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
import com.google.errorprone.annotations.Immutable;
2121
import dev.cel.common.annotations.Internal;
2222
import java.util.Collection;
23+
import java.util.Iterator;
24+
import java.util.List;
2325
import java.util.Map;
2426
import java.util.Optional;
27+
import java.util.RandomAccess;
2528
import java.util.function.Function;
2629

2730
/**
@@ -53,23 +56,46 @@ public static CelValueConverter getDefaultInstance() {
5356
* <p>The value may be a {@link CelValue}, a {@link Collection} or a {@link Map}.
5457
*/
5558
public Object maybeUnwrap(Object value) {
56-
if (value instanceof CelValue) {
57-
return unwrap((CelValue) value);
59+
if (value instanceof CelValue || value instanceof CelPreAdaptedList) {
60+
return value instanceof CelValue ? unwrap((CelValue) value) : value;
5861
}
5962

60-
Object mapped = mapContainer(value, maybeUnwrapFunction);
61-
if (mapped != value) {
62-
return mapped;
63-
}
64-
65-
return value;
63+
return mapContainer(value, maybeUnwrapFunction);
6664
}
6765

6866
/**
6967
* Maps a container (Collection or Map) by applying the provided mapper function to its elements.
7068
* Returns the original value if it's not a supported container.
7169
*/
7270
protected Object mapContainer(Object value, Function<Object, Object> mapper) {
71+
72+
// Zero allocation path for standard lists that support O(1) indexing
73+
// Generally, protobuf lists (backed by arrays) fall into this category
74+
if (value instanceof List && value instanceof RandomAccess) {
75+
List<Object> list = (List<Object>) value;
76+
for (int i = 0; i < list.size(); i++) {
77+
Object element = list.get(i);
78+
Object mapped = mapper.apply(element);
79+
80+
if (mapped != element) {
81+
ImmutableList.Builder<Object> builder =
82+
ImmutableList.builderWithExpectedSize(list.size());
83+
for (int j = 0; j < i; j++) {
84+
builder.add(list.get(j));
85+
}
86+
builder.add(mapped);
87+
for (int j = i + 1; j < list.size(); j++) {
88+
builder.add(mapper.apply(list.get(j)));
89+
}
90+
return builder.build();
91+
}
92+
}
93+
94+
// Zero allocations if unmodified
95+
return value;
96+
}
97+
98+
// Fallback for lists that are unordered
7399
if (value instanceof Collection) {
74100
Collection<Object> collection = (Collection<Object>) value;
75101
ImmutableList.Builder<Object> builder =
@@ -82,12 +108,32 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
82108

83109
if (value instanceof Map) {
84110
Map<Object, Object> map = (Map<Object, Object>) value;
85-
ImmutableMap.Builder<Object, Object> builder =
86-
ImmutableMap.builderWithExpectedSize(map.size());
87-
for (Map.Entry<Object, Object> entry : map.entrySet()) {
88-
builder.put(mapper.apply(entry.getKey()), mapper.apply(entry.getValue()));
111+
Iterator<Map.Entry<Object, Object>> iterator = map.entrySet().iterator();
112+
113+
while (iterator.hasNext()) {
114+
Map.Entry<Object, Object> entry = iterator.next();
115+
Object mappedKey = mapper.apply(entry.getKey());
116+
Object mappedValue = mapper.apply(entry.getValue());
117+
118+
if (mappedKey != entry.getKey() || mappedValue != entry.getValue()) {
119+
ImmutableMap.Builder<Object, Object> builder =
120+
ImmutableMap.builderWithExpectedSize(map.size());
121+
122+
for (Map.Entry<Object, Object> prevEntry : map.entrySet()) {
123+
if (prevEntry.getKey() == entry.getKey()) {
124+
break;
125+
}
126+
builder.put(mapper.apply(prevEntry.getKey()), mapper.apply(prevEntry.getValue()));
127+
}
128+
builder.put(mappedKey, mappedValue);
129+
while (iterator.hasNext()) {
130+
Map.Entry<Object, Object> nextEntry = iterator.next();
131+
builder.put(mapper.apply(nextEntry.getKey()), mapper.apply(nextEntry.getValue()));
132+
}
133+
return builder.buildOrThrow();
134+
}
89135
}
90-
return builder.buildOrThrow();
136+
return value;
91137
}
92138

93139
return value;
@@ -96,7 +142,7 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
96142
public Object toRuntimeValue(Object value) {
97143
Preconditions.checkNotNull(value);
98144

99-
if (value instanceof CelValue) {
145+
if (value instanceof CelValue || value instanceof CelPreAdaptedList) {
100146
return value;
101147
}
102148

common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ public Object fromProtoMessageFieldToCelValue(Message message, FieldDescriptor f
167167
break;
168168
}
169169

170+
if (fieldDescriptor.isRepeated()) {
171+
switch (fieldDescriptor.getType()) {
172+
case INT64:
173+
case BOOL:
174+
case STRING:
175+
case DOUBLE:
176+
return CelPreAdaptedList.wrap((List<?>) result);
177+
default:
178+
break;
179+
}
180+
}
181+
170182
return toRuntimeValue(result);
171183
}
172184

0 commit comments

Comments
 (0)