Skip to content

Commit a11f838

Browse files
test(go,js): benchmarks
1 parent 4c04dd6 commit a11f838

File tree

10 files changed

+500
-0
lines changed

10 files changed

+500
-0
lines changed

Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,29 @@ clean:
6969
$(MAKE) -C openfeature-provider/java clean
7070
$(MAKE) -C openfeature-provider/go clean
7171

72+
.PHONY: js-build
73+
js-build:
74+
$(MAKE) -C openfeature-provider/js build
75+
76+
.PHONY: go-bench js-bench
77+
go-bench:
78+
@status=0; \
79+
docker compose up --build \
80+
--abort-on-container-exit \
81+
--exit-code-from go-bench \
82+
--attach go-bench --attach mock-go \
83+
go-bench mock-go || status=$$?; \
84+
docker compose down --remove-orphans --volumes; \
85+
exit $$status
86+
87+
js-bench: js-build
88+
@status=0; \
89+
docker compose up --build \
90+
--abort-on-container-exit \
91+
--exit-code-from js-bench \
92+
--attach js-bench --attach mock-js \
93+
js-bench mock-js || status=$$?; \
94+
docker compose down --remove-orphans --volumes; \
95+
exit $$status
96+
7297
.DEFAULT_GOAL := all

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ make cloudflare
6868

6969
You can then integrate with Wrangler using `confidence-cloudflare-resolver/wrangler.toml`.
7070

71+
## Benchmarks (WIP)
72+
73+
Small local benchmarks exist for Go and Node.js to validate end-to-end wiring. They are a work-in-progress and do not produce meaningful or representative performance numbers yet.
74+
75+
Run with Docker (streams all logs, cleans up containers afterward):
76+
77+
```bash
78+
# Go benchmark
79+
make go-bench
80+
81+
# Node.js benchmark
82+
make js-bench
83+
```
84+
85+
Notes:
86+
- Each target starts a dedicated mock server container and a one-shot bench container, then tears everything down.
87+
- Use `docker compose up ... go-bench` or `... js-bench` to run them individually without Make.
88+
7189
## License
7290

7391
See `LICENSE` for details.

docker-compose.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
services:
2+
mock-go:
3+
build:
4+
context: .
5+
dockerfile: mock-support-server/Dockerfile
6+
environment:
7+
- PORT=8081
8+
- RESOLVER_STATE_PB=/data/resolver_state_current.pb
9+
- SIGNED_STATE_URI=http://mock-go:8081/state
10+
11+
go-bench:
12+
build:
13+
context: .
14+
dockerfile: openfeature-provider/go/bench/Dockerfile
15+
depends_on:
16+
- mock-go
17+
command: ["-mock-grpc", "mock-go:8081", "-threads", "1", "-flag", "tutorial-feature", "-client-secret", "mkjJruAATQWjeY7foFIWfVAcBWnci2YF"]
18+
19+
mock-js:
20+
build:
21+
context: .
22+
dockerfile: mock-support-server/Dockerfile
23+
environment:
24+
- PORT=8081
25+
- RESOLVER_STATE_PB=/data/resolver_state_current.pb
26+
- SIGNED_STATE_URI=https://storage.googleapis.com/state
27+
28+
js-bench:
29+
build:
30+
context: .
31+
dockerfile: openfeature-provider/js/bench/Dockerfile
32+
depends_on:
33+
- mock-js
34+
command: ["-mock-http", "http://mock-js:8081", "-flag", "tutorial-feature", "-client-secret", "mkjJruAATQWjeY7foFIWfVAcBWnci2YF"]
35+
36+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM golang:1.24-alpine3.22 AS build
2+
WORKDIR /src
3+
4+
# Prepare module files in correct relative locations to satisfy the replace directive
5+
COPY openfeature-provider/go/bench/go.mod openfeature-provider/go/bench/go.sum ./openfeature-provider/go/bench/
6+
COPY openfeature-provider/go/confidence/go.mod openfeature-provider/go/confidence/go.sum ./openfeature-provider/go/confidence/
7+
8+
# Prime module cache
9+
RUN --mount=type=cache,target=/go/pkg/mod \
10+
go mod download -C ./openfeature-provider/go/bench
11+
12+
# Copy sources (bench + local provider, including embedded wasm)
13+
COPY openfeature-provider/go/confidence ./openfeature-provider/go/confidence
14+
COPY openfeature-provider/go/bench ./openfeature-provider/go/bench
15+
16+
# Build static bench binary
17+
RUN --mount=type=cache,target=/go/pkg/mod \
18+
--mount=type=cache,target=/root/.cache/go-build \
19+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
20+
go build -C ./openfeature-provider/go/bench -o /out/bench .
21+
22+
FROM alpine:3.22
23+
RUN apk add --no-cache ca-certificates
24+
COPY --from=build /out/bench /bench
25+
ENTRYPOINT ["/bench"]
26+
27+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module github.com/spotify/confidence-resolver-rust/openfeature-provider/go/bench
2+
3+
go 1.24.0
4+
5+
require (
6+
github.com/open-feature/go-sdk v1.16.0
7+
github.com/spotify/confidence-resolver-rust/openfeature-provider/go/confidence v0.0.0
8+
google.golang.org/grpc v1.75.1
9+
)
10+
11+
require (
12+
github.com/go-logr/logr v1.4.3 // indirect
13+
github.com/spotify/confidence-resolver/openfeature-provider/go/confidence v0.1.0 // indirect
14+
github.com/tetratelabs/wazero v1.9.0 // indirect
15+
go.uber.org/mock v0.6.0 // indirect
16+
golang.org/x/net v0.44.0 // indirect
17+
golang.org/x/sys v0.36.0 // indirect
18+
golang.org/x/text v0.29.0 // indirect
19+
google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda // indirect
20+
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
21+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
22+
google.golang.org/protobuf v1.36.10 // indirect
23+
)
24+
25+
replace github.com/spotify/confidence-resolver-rust/openfeature-provider/go/confidence => ../confidence
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
2+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
4+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
5+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
6+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
7+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
8+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
9+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
10+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11+
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
12+
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
13+
github.com/spotify/confidence-resolver/openfeature-provider/go/confidence v0.1.0 h1:U5iXqBFZ/9mXQ0wFEpOetiwJKVCcLL5oAtN5xnFC4+Y=
14+
github.com/spotify/confidence-resolver/openfeature-provider/go/confidence v0.1.0/go.mod h1:NkvUpOOTuAlmsfQ3WfqYkIVL+G3pVds4HuwXhbupnLY=
15+
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
16+
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
17+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
18+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
19+
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
20+
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
21+
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
22+
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
23+
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
24+
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
25+
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
26+
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
27+
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
28+
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
29+
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
30+
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
31+
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
32+
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
33+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
34+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
35+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
36+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
37+
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
38+
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
39+
google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda h1:fQ3VVQ11pb84nu0o/8wD6oZq13Q6+HK30P+9GSRlrqk=
40+
google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda/go.mod h1:1Ic78BnpzY8OaTCmzxJDP4qC9INZPbGZl+54RKjtyeI=
41+
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
42+
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
43+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
44+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
45+
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
46+
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
47+
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
48+
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"os/signal"
9+
"runtime"
10+
"sync"
11+
"sync/atomic"
12+
"syscall"
13+
"time"
14+
15+
openfeature "github.com/open-feature/go-sdk/openfeature"
16+
confidence "github.com/spotify/confidence-resolver-rust/openfeature-provider/go/confidence"
17+
"google.golang.org/grpc"
18+
"google.golang.org/grpc/credentials/insecure"
19+
)
20+
21+
type stats struct {
22+
completed uint64
23+
errors uint64
24+
}
25+
26+
func main() {
27+
var (
28+
mockGRPCAddr string
29+
durationSeconds int
30+
warmupSeconds int
31+
threads int
32+
gomaxprocs int
33+
flagKey string
34+
clientSecret string
35+
apiClientID string
36+
apiClientSecret string
37+
pollInterval int
38+
)
39+
40+
flag.StringVar(&mockGRPCAddr, "mock-grpc", "localhost:8081", "mock support server gRPC address host:port")
41+
flag.IntVar(&durationSeconds, "duration", 30, "benchmark duration in seconds (excludes warmup)")
42+
flag.IntVar(&warmupSeconds, "warmup", 5, "warmup duration in seconds before measurement")
43+
flag.IntVar(&threads, "threads", runtime.NumCPU(), "number of concurrent worker goroutines")
44+
flag.IntVar(&gomaxprocs, "gomaxprocs", 0, "set GOMAXPROCS (0=leave default)")
45+
flag.StringVar(&flagKey, "flag", "example-flag", "flag key (without 'flags/' prefix)")
46+
flag.StringVar(&clientSecret, "client-secret", "secret", "client secret for request signing")
47+
flag.StringVar(&apiClientID, "api-client-id", "mock-client", "API client ID for token requests")
48+
flag.StringVar(&apiClientSecret, "api-client-secret", "mock-secret", "API client secret for token requests")
49+
flag.IntVar(&pollInterval, "poll-interval", 10, "resolver state/log poll interval in seconds (env override)")
50+
flag.Parse()
51+
52+
if gomaxprocs > 0 {
53+
runtime.GOMAXPROCS(gomaxprocs)
54+
}
55+
if threads < 1 {
56+
threads = 1
57+
}
58+
if warmupSeconds < 0 {
59+
warmupSeconds = 0
60+
}
61+
if durationSeconds < 1 {
62+
durationSeconds = 1
63+
}
64+
65+
// Ensure state/log polling is exercised during the run
66+
// os.Setenv("CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS", fmt.Sprintf("%d", pollInterval))
67+
68+
ctx := context.Background()
69+
70+
// Build a provider wired to the mock server via ConnFactory. The factory ignores the
71+
// target passed by the provider and always dials the mock address with insecure creds,
72+
// while preserving any supplied interceptors (e.g., JWT auth).
73+
connFactory := func(ctx context.Context, _ string, defaultOpts []grpc.DialOption) (grpc.ClientConnInterface, error) {
74+
// Keep the default options (notably auth interceptors), but ensure we use insecure transport
75+
// to match the mock server and override any TLS transport credentials.
76+
opts := append([]grpc.DialOption{}, defaultOpts...)
77+
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
78+
return grpc.NewClient(mockGRPCAddr, opts...)
79+
}
80+
81+
provider, err := confidence.NewProvider(ctx, confidence.ProviderConfig{
82+
APIClientID: apiClientID,
83+
APIClientSecret: apiClientSecret,
84+
ClientSecret: clientSecret,
85+
ConnFactory: connFactory,
86+
})
87+
if err != nil {
88+
fmt.Fprintf(os.Stderr, "failed to create provider: %v\n", err)
89+
os.Exit(1)
90+
}
91+
92+
// Minimal evaluation context; you can extend with attributes to exercise targeting
93+
evalCtx := openfeature.FlattenedContext{"targetingKey": "tutorial_visitor", "visitor_id": "tutorial_visitor"}
94+
95+
// Prepare cancellation on SIGINT/SIGTERM
96+
sigCh := make(chan os.Signal, 1)
97+
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
98+
99+
// Warmup (abort on first error)
100+
if warmupSeconds > 0 {
101+
warmupCtx, cancel := context.WithTimeout(ctx, time.Duration(warmupSeconds)*time.Second)
102+
var warm stats
103+
runWorkers(warmupCtx, provider, flagKey, evalCtx, threads, &warm, cancel, true)
104+
cancel()
105+
if atomic.LoadUint64(&warm.errors) > 0 {
106+
fmt.Fprintf(os.Stderr, "aborting: error during warmup\n")
107+
os.Exit(1)
108+
}
109+
}
110+
111+
// Measurement
112+
measureCtx, cancelMeasure := context.WithTimeout(ctx, time.Duration(durationSeconds)*time.Second)
113+
defer cancelMeasure()
114+
115+
var s stats
116+
// Abort early on signal
117+
go func() {
118+
select {
119+
case <-sigCh:
120+
cancelMeasure()
121+
case <-measureCtx.Done():
122+
}
123+
}()
124+
125+
start := time.Now()
126+
runWorkers(measureCtx, provider, flagKey, evalCtx, threads, &s, cancelMeasure, true)
127+
elapsed := time.Since(start)
128+
provider.Shutdown()
129+
130+
completed := atomic.LoadUint64(&s.completed)
131+
errs := atomic.LoadUint64(&s.errors)
132+
qps := float64(completed) / elapsed.Seconds()
133+
134+
fmt.Printf("flag=%s threads=%d duration=%s ops=%d errors=%d throughput=%.0f ops/s\n",
135+
flagKey, threads, elapsed.Truncate(time.Millisecond), completed, errs, qps)
136+
}
137+
138+
func runWorkers(ctx context.Context, provider *confidence.LocalResolverProvider, flagKey string, evalCtx openfeature.FlattenedContext, threads int, s *stats, cancel context.CancelFunc, abortOnError bool) {
139+
wg := sync.WaitGroup{}
140+
wg.Add(threads)
141+
for i := 0; i < threads; i++ {
142+
go func() {
143+
defer wg.Done()
144+
for {
145+
select {
146+
case <-ctx.Done():
147+
return
148+
default:
149+
res := provider.ObjectEvaluation(context.Background(), flagKey, nil, evalCtx)
150+
if s != nil {
151+
atomic.AddUint64(&s.completed, 1)
152+
// fmt.Printf("reason %s", res.Reason)
153+
if res.Reason == openfeature.ErrorReason {
154+
atomic.AddUint64(&s.errors, 1)
155+
if abortOnError && cancel != nil {
156+
cancel()
157+
return
158+
}
159+
}
160+
}
161+
}
162+
}
163+
}()
164+
}
165+
wg.Wait()
166+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM node:20-alpine3.22
2+
3+
WORKDIR /app
4+
5+
# Install runtime deps
6+
COPY openfeature-provider/js/bench/package.json ./package.json
7+
RUN npm install --omit=dev
8+
9+
# Copy provider dist (JS + WASM)
10+
COPY openfeature-provider/js/dist ./dist
11+
12+
# Copy entire bench directory so relative imports (../dist) remain valid
13+
COPY openfeature-provider/js/bench ./bench
14+
15+
ENV NODE_ENV=production
16+
17+
ENTRYPOINT ["node", "bench/bench.mjs"]
18+
19+

0 commit comments

Comments
 (0)