Skip to content

Commit 796508d

Browse files
committed
test(stability): add query benchmarks + a concurrent read/write test
Benchmarks (internal/graphstore, internal/query) for the hot read paths — QueryEntities, QueryTopo, and the full Service.Execute entity path — so perf regressions on large result sets become visible. 'make bench' runs them (smoke by default, BENCHTIME-overridable); CI runs them non-blocking. A concurrent read/write test stresses the memory store's RWMutex; under 'go test -race' it is a data-race regression guard for the shared maps (verified race-clean).
1 parent f9f5e28 commit 796508d

5 files changed

Lines changed: 190 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ jobs:
7878
- name: Run CI
7979
run: make ci
8080

81+
# Smoke-run the benchmarks so they keep compiling and executing. Non-blocking
82+
# for now (no perf thresholds yet); drop continue-on-error to gate on perf.
83+
- name: Run benchmarks (smoke)
84+
run: make bench
85+
continue-on-error: true
86+
8187
- name: Run Playwright UI tests
8288
run: |
8389
make quickstart &

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.PHONY: help check-env install-env setup setup-ui expand doc docs-schema docs-schema-check example-validate check-manifest
2-
.PHONY: build build-service build-cli install-cli build-ui build-sdk-go dev quickstart dev-api dev-web deploy serve-ui status stop-all stop-dev stop-deploy test test-service test-ui test-ui-e2e test-capability test-quickstart-health test-ladybug verify verify-go verify-python verify-java guard ci clean
2+
.PHONY: build build-service build-cli install-cli build-ui build-sdk-go dev quickstart dev-api dev-web deploy serve-ui status stop-all stop-dev stop-deploy test test-service bench test-ui test-ui-e2e test-capability test-quickstart-health test-ladybug verify verify-go verify-python verify-java guard ci clean
33

44
VENV_PYTHON := .venv/bin/python
55
CONDA_PYTHON := $(if $(CONDA_PREFIX),$(CONDA_PREFIX)/bin/python)
@@ -135,6 +135,13 @@ serve-ui: build-ui
135135
test-service:
136136
go test ./...
137137

138+
# Run benchmarks. Default is a smoke run (a few iterations: ensures they compile
139+
# and execute without panicking). Raise BENCHTIME, e.g. `make bench BENCHTIME=2s`,
140+
# for real measurements.
141+
BENCHTIME ?= 10x
142+
bench:
143+
go test -run '^$$' -bench=. -benchtime=$(BENCHTIME) ./internal/query/... ./internal/graphstore/...
144+
138145
test-ui:
139146
@PNPM="$(PNPM)" bash ./scripts/env.sh web-build
140147

internal/graphstore/bench_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package graphstore
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/alibaba/UnifiedModel/pkg/model"
9+
)
10+
11+
// seedMemoryStore fills a workspace with n entities and n relations (a ring) so
12+
// the scan paths have realistic work to do.
13+
func seedMemoryStore(tb testing.TB, store *MemoryStore, n int) {
14+
tb.Helper()
15+
ctx := context.Background()
16+
ents := make([]model.EntityPayload, n)
17+
rels := make([]model.RelationPayload, n)
18+
for i := 0; i < n; i++ {
19+
id := fmt.Sprintf("%032x", i+1)
20+
ents[i] = entityPayload(id, "Update", 100, 200, map[string]any{"display_name": fmt.Sprintf("svc-%d", i)})
21+
}
22+
if _, err := store.WriteEntities(ctx, model.EntityWriteBatch{Workspace: "demo", Entities: ents}); err != nil {
23+
tb.Fatalf("seed entities: %v", err)
24+
}
25+
for i := 0; i < n; i++ {
26+
src := fmt.Sprintf("%032x", i+1)
27+
dst := fmt.Sprintf("%032x", (i+1)%n+1)
28+
rels[i] = relationPayload(src, dst, "Update", 100, 200, nil)
29+
}
30+
if _, err := store.WriteRelations(ctx, model.RelationWriteBatch{Workspace: "demo", Relations: rels}); err != nil {
31+
tb.Fatalf("seed relations: %v", err)
32+
}
33+
}
34+
35+
func BenchmarkQueryEntities(b *testing.B) {
36+
store := NewMemoryStore()
37+
seedMemoryStore(b, store, 2000)
38+
ctx := context.Background()
39+
plan := model.EntityQueryPlan{Workspace: "demo", Limit: 1000}
40+
b.ReportAllocs()
41+
b.ResetTimer()
42+
for i := 0; i < b.N; i++ {
43+
if _, err := store.QueryEntities(ctx, plan); err != nil {
44+
b.Fatal(err)
45+
}
46+
}
47+
}
48+
49+
func BenchmarkQueryTopo(b *testing.B) {
50+
store := NewMemoryStore()
51+
seedMemoryStore(b, store, 2000)
52+
ctx := context.Background()
53+
plan := model.TopoQueryPlan{Workspace: "demo", Limit: 1000}
54+
b.ReportAllocs()
55+
b.ResetTimer()
56+
for i := 0; i < b.N; i++ {
57+
if _, err := store.QueryTopo(ctx, plan); err != nil {
58+
b.Fatal(err)
59+
}
60+
}
61+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package graphstore
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"testing"
8+
9+
"github.com/alibaba/UnifiedModel/pkg/model"
10+
)
11+
12+
// TestConcurrentReadWrite stresses the memory store's RWMutex with overlapping
13+
// readers and writers. On its own it asserts no operation errors; under `go test
14+
// -race` (the gate's mode) it is a data-race regression guard for the shared
15+
// entity/relation maps.
16+
func TestConcurrentReadWrite(t *testing.T) {
17+
store := NewMemoryStore()
18+
seedMemoryStore(t, store, 200)
19+
20+
const (
21+
readers = 24
22+
writers = 8
23+
iters = 100
24+
)
25+
errCh := make(chan error, readers+writers)
26+
var wg sync.WaitGroup
27+
28+
for r := 0; r < readers; r++ {
29+
wg.Add(1)
30+
go func() {
31+
defer wg.Done()
32+
ctx := context.Background()
33+
for i := 0; i < iters; i++ {
34+
if _, err := store.QueryEntities(ctx, model.EntityQueryPlan{Workspace: "demo", Limit: 100}); err != nil {
35+
errCh <- fmt.Errorf("query entities: %w", err)
36+
return
37+
}
38+
if _, err := store.QueryTopo(ctx, model.TopoQueryPlan{Workspace: "demo", Limit: 100}); err != nil {
39+
errCh <- fmt.Errorf("query topo: %w", err)
40+
return
41+
}
42+
}
43+
}()
44+
}
45+
for w := 0; w < writers; w++ {
46+
wg.Add(1)
47+
go func(w int) {
48+
defer wg.Done()
49+
ctx := context.Background()
50+
for i := 0; i < iters; i++ {
51+
id := fmt.Sprintf("%032x", 1_000_000+w*iters+i)
52+
if _, err := store.WriteEntities(ctx, model.EntityWriteBatch{
53+
Workspace: "demo",
54+
Entities: []model.EntityPayload{entityPayload(id, "Update", 100, 200, nil)},
55+
}); err != nil {
56+
errCh <- fmt.Errorf("write entities: %w", err)
57+
return
58+
}
59+
}
60+
}(w)
61+
}
62+
63+
wg.Wait()
64+
close(errCh)
65+
for err := range errCh {
66+
t.Fatalf("concurrent operation failed: %v", err)
67+
}
68+
}

internal/query/bench_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package query
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/alibaba/UnifiedModel/internal/graphstore"
9+
"github.com/alibaba/UnifiedModel/pkg/model"
10+
)
11+
12+
// BenchmarkExecuteEntityQuery measures the full read entry point: normalize ->
13+
// parse -> plan -> executor -> store scan, returning matched rows.
14+
func BenchmarkExecuteEntityQuery(b *testing.B) {
15+
store := graphstore.NewMemoryStore()
16+
ctx := context.Background()
17+
ents := make([]model.EntityPayload, 1000)
18+
for i := range ents {
19+
ents[i] = model.EntityPayload{
20+
"__domain__": "devops",
21+
"__entity_type__": "devops.service",
22+
"__entity_id__": fmt.Sprintf("%032x", i+1),
23+
"__method__": "Update",
24+
"__first_observed_time__": int64(100),
25+
"__last_observed_time__": int64(200),
26+
"__keep_alive_seconds__": int64(60),
27+
"display_name": fmt.Sprintf("svc-%d", i),
28+
}
29+
}
30+
if _, err := store.WriteEntities(ctx, model.EntityWriteBatch{Workspace: "demo", Entities: ents}); err != nil {
31+
b.Fatalf("seed: %v", err)
32+
}
33+
34+
svc := NewService(store)
35+
req := model.QueryRequest{Query: ".entity with(domain='devops', name='devops.service') | limit 1000"}
36+
b.ReportAllocs()
37+
b.ResetTimer()
38+
for i := 0; i < b.N; i++ {
39+
res, err := svc.Execute(ctx, "demo", req)
40+
if err != nil {
41+
b.Fatal(err)
42+
}
43+
if len(res.Rows) == 0 {
44+
b.Fatal("expected rows")
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)