Skip to content

Commit d7df358

Browse files
committed
Add benchmark comparing CEL vs FIELD vs Direct filter_state access
1 parent 82ed55e commit d7df358

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

source/extensions/filters/http/peer_metadata/BUILD

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
load(
1919
"@envoy//bazel:envoy_build_system.bzl",
20+
"envoy_cc_benchmark_binary",
2021
"envoy_cc_library",
2122
"envoy_cc_test",
2223
"envoy_proto_library",
@@ -65,3 +66,22 @@ envoy_cc_test(
6566
"@envoy//test/test_common:logging_lib",
6667
],
6768
)
69+
70+
envoy_cc_benchmark_binary(
71+
name = "filter_state_benchmark",
72+
srcs = ["filter_state_benchmark.cc"],
73+
repository = "@envoy",
74+
deps = [
75+
":filter_lib",
76+
"//extensions/common:metadata_object_lib",
77+
"@envoy//source/common/formatter:formatter_extension_lib",
78+
"@envoy//source/common/formatter:substitution_formatter_lib",
79+
"@envoy//source/common/stream_info:stream_info_lib",
80+
"@envoy//source/extensions/filters/common/expr:cel_state_lib",
81+
"@envoy//source/extensions/formatter/cel:config",
82+
"@envoy//test/common/stream_info:test_util",
83+
"@envoy//test/mocks:common_lib",
84+
"@envoy//test/mocks/server:factory_context_mocks",
85+
"@envoy//test/test_common:utility_lib",
86+
],
87+
)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright Istio Authors. All Rights Reserved.
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+
// http://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+
#include "source/extensions/filters/http/peer_metadata/filter.h"
16+
17+
#include "source/common/formatter/substitution_formatter.h"
18+
#include "source/extensions/filters/common/expr/cel_state.h"
19+
#include "extensions/common/metadata_object.h"
20+
21+
#include "test/common/stream_info/test_util.h"
22+
#include "test/mocks/common.h"
23+
#include "test/mocks/server/factory_context.h"
24+
#include "test/test_common/utility.h"
25+
26+
#include "benchmark/benchmark.h"
27+
28+
namespace Envoy {
29+
namespace Extensions {
30+
namespace HttpFilters {
31+
namespace PeerMetadata {
32+
33+
namespace {
34+
35+
// Helper to create a WorkloadMetadataObject with realistic test data
36+
std::unique_ptr<Istio::Common::WorkloadMetadataObject> makeWorkloadMetadata() {
37+
return std::make_unique<Istio::Common::WorkloadMetadataObject>(
38+
"sleep-v1-12345-abcde", // instance_name
39+
"cluster1", // cluster_name
40+
"default", // namespace_name
41+
"sleep-v1", // workload_name
42+
"sleep", // canonical_name
43+
"v1", // canonical_revision
44+
"sleep", // app_name
45+
"v1", // app_version
46+
Istio::Common::WorkloadType::Pod, // workload_type
47+
"spiffe://cluster.local/ns/default/sa/sleep", // identity
48+
"us-west1", // region
49+
"us-west1-a" // zone
50+
);
51+
}
52+
53+
// Setup stream info with filter state for CEL access
54+
void setupCelFilterState(Envoy::StreamInfo::StreamInfo& stream_info) {
55+
auto metadata = makeWorkloadMetadata();
56+
auto proto = metadata->serializeAsProto();
57+
58+
// CEL access requires CelState wrapper under "downstream_peer" key
59+
auto cel_state =
60+
std::make_unique<Filters::Common::Expr::CelState>(FilterConfig::peerInfoPrototype());
61+
cel_state->setValue(absl::string_view(proto->SerializeAsString()));
62+
63+
stream_info.filterState()->setData(
64+
std::string(Istio::Common::DownstreamPeer), std::move(cel_state),
65+
StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain);
66+
}
67+
68+
// Setup stream info with filter state for FIELD access
69+
void setupFieldFilterState(Envoy::StreamInfo::StreamInfo& stream_info) {
70+
auto metadata = makeWorkloadMetadata();
71+
72+
// FIELD access uses WorkloadMetadataObject under "downstream_peer_obj" key
73+
stream_info.filterState()->setData(
74+
std::string(Istio::Common::DownstreamPeerObj), std::move(metadata),
75+
StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain);
76+
}
77+
78+
} // namespace
79+
80+
// Benchmark CEL accessor for filter_state.downstream_peer.workload
81+
// NOLINTNEXTLINE(readability-identifier-naming)
82+
static void BM_FilterState_CEL(benchmark::State& state) {
83+
testing::NiceMock<MockTimeSystem> time_system;
84+
NiceMock<Server::Configuration::MockFactoryContext> context;
85+
ScopedThreadLocalServerContextSetter server_context_setter(context.server_factory_context_);
86+
87+
Envoy::TestStreamInfo stream_info(time_system);
88+
89+
setupCelFilterState(stream_info);
90+
91+
// CEL format: %CEL(filter_state.downstream_peer.workload)%
92+
const std::string format = "%CEL(filter_state.downstream_peer.workload)%";
93+
auto formatter = *Formatter::FormatterImpl::create(format, false);
94+
95+
Formatter::Context formatter_context;
96+
size_t total_bytes_allocated = 0;
97+
98+
for (auto _ : state) { // NOLINT
99+
std::string result = formatter->format(formatter_context, stream_info);
100+
// Count string allocation: capacity is usually result.size() rounded up to power of 2
101+
// For small strings like "sleep-v1", this is typically 16-32 bytes
102+
total_bytes_allocated += result.capacity();
103+
benchmark::DoNotOptimize(result);
104+
}
105+
106+
// Report memory allocated per iteration
107+
state.SetBytesProcessed(total_bytes_allocated);
108+
state.SetLabel("alloc_per_iter=" + std::to_string(total_bytes_allocated / state.iterations()) +
109+
"B");
110+
}
111+
BENCHMARK(BM_FilterState_CEL);
112+
113+
// Benchmark FIELD accessor for filter_state downstream_peer workload
114+
// NOLINTNEXTLINE(readability-identifier-naming)
115+
static void BM_FilterState_FIELD(benchmark::State& state) {
116+
testing::NiceMock<MockTimeSystem> time_system;
117+
NiceMock<Server::Configuration::MockFactoryContext> context;
118+
ScopedThreadLocalServerContextSetter server_context_setter(context.server_factory_context_);
119+
120+
Envoy::TestStreamInfo stream_info(time_system);
121+
122+
setupFieldFilterState(stream_info);
123+
124+
// FIELD format: %FILTER_STATE(downstream_peer_obj:FIELD:workload)%
125+
const std::string format = "%FILTER_STATE(downstream_peer_obj:FIELD:workload)%";
126+
auto formatter = *Formatter::FormatterImpl::create(format, false);
127+
128+
Formatter::Context formatter_context;
129+
size_t total_bytes_allocated = 0;
130+
131+
for (auto _ : state) { // NOLINT
132+
std::string result = formatter->format(formatter_context, stream_info);
133+
total_bytes_allocated += result.capacity();
134+
benchmark::DoNotOptimize(result);
135+
}
136+
137+
state.SetBytesProcessed(total_bytes_allocated);
138+
state.SetLabel("alloc_per_iter=" + std::to_string(total_bytes_allocated / state.iterations()) +
139+
"B");
140+
}
141+
BENCHMARK(BM_FilterState_FIELD);
142+
143+
// Benchmark baseline - accessing filter state directly without formatter
144+
// NOLINTNEXTLINE(readability-identifier-naming)
145+
static void BM_FilterState_Direct(benchmark::State& state) {
146+
testing::NiceMock<MockTimeSystem> time_system;
147+
NiceMock<Server::Configuration::MockFactoryContext> context;
148+
ScopedThreadLocalServerContextSetter server_context_setter(context.server_factory_context_);
149+
150+
Envoy::TestStreamInfo stream_info(time_system);
151+
152+
setupFieldFilterState(stream_info);
153+
154+
size_t total_bytes_read = 0;
155+
156+
for (auto _ : state) { // NOLINT
157+
const auto* obj =
158+
stream_info.filterState()->getDataReadOnly<Istio::Common::WorkloadMetadataObject>(
159+
std::string(Istio::Common::DownstreamPeerObj));
160+
if (obj) {
161+
// Direct access doesn't allocate - just reads the string_view
162+
total_bytes_read += obj->workload_name_.length();
163+
}
164+
}
165+
166+
state.SetBytesProcessed(total_bytes_read);
167+
state.SetLabel("alloc_per_iter=0B (no allocation, direct access)");
168+
benchmark::DoNotOptimize(total_bytes_read);
169+
}
170+
BENCHMARK(BM_FilterState_Direct);
171+
172+
} // namespace PeerMetadata
173+
} // namespace HttpFilters
174+
} // namespace Extensions
175+
} // namespace Envoy

0 commit comments

Comments
 (0)