diff --git a/apps/workspace-engine/pkg/workspace/relationships/compute/incremental.go b/apps/workspace-engine/pkg/workspace/relationships/compute/incremental.go index c4c3002cc..4b59adb99 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/compute/incremental.go +++ b/apps/workspace-engine/pkg/workspace/relationships/compute/incremental.go @@ -12,19 +12,26 @@ func FindRemovedRelations( oldRelations []*relationships.EntityRelation, newRelations []*relationships.EntityRelation, ) []*relationships.EntityRelation { - removedRelations := make([]*relationships.EntityRelation, 0) + removedRelations := make([]*relationships.EntityRelation, 0, len(oldRelations)) + if len(oldRelations) == 0 { + return removedRelations + } + + if len(newRelations) == 0 { + return append(removedRelations, oldRelations...) + } + + newRelationKeys := make(map[string]struct{}, len(newRelations)) + for _, newRelation := range newRelations { + newRelationKeys[newRelation.Key()] = struct{}{} + } + for _, oldRelation := range oldRelations { - found := false - for _, newRelation := range newRelations { - if oldRelation.Key() == newRelation.Key() { - found = true - break - } - } - if !found { + if _, found := newRelationKeys[oldRelation.Key()]; !found { removedRelations = append(removedRelations, oldRelation) } } + return removedRelations } diff --git a/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_bench_test.go b/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_bench_test.go new file mode 100644 index 000000000..4b8642e3a --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_bench_test.go @@ -0,0 +1,40 @@ +package compute + +import ( + "context" + "fmt" + "testing" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/relationships" +) + +func BenchmarkFindRemovedRelations(b *testing.B) { + ctx := context.Background() + + rule := &oapi.RelationshipRule{Id: "rule-1"} + from := relationships.NewResourceEntity(&oapi.Resource{Id: "resource-1"}) + + oldRelations := make([]*relationships.EntityRelation, 0, 20000) + newRelations := make([]*relationships.EntityRelation, 0, 15000) + + for i := 0; i < 20000; i++ { + to := relationships.NewDeploymentEntity(&oapi.Deployment{Id: fmt.Sprintf("deployment-%d", i)}) + relation := &relationships.EntityRelation{ + Rule: rule, + From: from, + To: to, + } + oldRelations = append(oldRelations, relation) + if i%4 != 0 { + newRelations = append(newRelations, relation) + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + removed := FindRemovedRelations(ctx, oldRelations, newRelations) + if len(removed) == 0 { + b.Fatal("unexpected empty result") + } + } +} diff --git a/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_test.go b/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_test.go index 6a3d9101b..8abe86ce6 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_test.go +++ b/apps/workspace-engine/pkg/workspace/relationships/compute/incremental_test.go @@ -586,7 +586,6 @@ func TestFindRemovedRelations_NoneRemoved(t *testing.T) { assert.Empty(t, removedRelations) } - func TestFilterEntitiesByTypeAndSelector(t *testing.T) { ctx := context.Background() diff --git a/apps/workspace-engine/pkg/workspace/relationships/compute/rule.go b/apps/workspace-engine/pkg/workspace/relationships/compute/rule.go index e15f46ee7..bc91cfeb8 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/compute/rule.go +++ b/apps/workspace-engine/pkg/workspace/relationships/compute/rule.go @@ -27,7 +27,13 @@ func FindRuleRelationships(ctx context.Context, rule *oapi.RelationshipRule, ent return nil, nil } - entityMapCache := relationships.BuildEntityMapCache(entities) + var entityMapCache relationships.EntityMapCache + if matcherUsesCel(rule) { + cacheEntities := make([]*oapi.RelatableEntity, 0, len(fromEntities)+len(toEntities)) + cacheEntities = append(cacheEntities, fromEntities...) + cacheEntities = append(cacheEntities, toEntities...) + entityMapCache = relationships.BuildEntityMapCache(cacheEntities) + } // For small datasets, use serial processing totalPairs := len(fromEntities) * len(toEntities) @@ -64,6 +70,14 @@ func FindRuleRelationships(ctx context.Context, rule *oapi.RelationshipRule, ent return allRelations, nil } +func matcherUsesCel(rule *oapi.RelationshipRule) bool { + if rule == nil { + return false + } + cm, err := rule.Matcher.AsCelMatcher() + return err == nil && cm.Cel != "" +} + // matchFromEntityToAll matches a single fromEntity against all toEntities. // This function runs in parallel for different fromEntities. func matchFromEntityToAll( diff --git a/apps/workspace-engine/pkg/workspace/relationships/matcher.go b/apps/workspace-engine/pkg/workspace/relationships/matcher.go index 3fcf76acd..21c110d2c 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/matcher.go +++ b/apps/workspace-engine/pkg/workspace/relationships/matcher.go @@ -120,13 +120,17 @@ func MatchesWithCache(ctx context.Context, matcher *oapi.RelationshipRule_Matche return true } -// BuildEntityMapCache pre-computes map representations for all entities -// This is expensive but only needs to be done once per rule evaluation +// BuildEntityMapCache pre-computes map representations for provided entities. +// This is expensive but only needs to be done once per rule evaluation. func BuildEntityMapCache(entities []*oapi.RelatableEntity) EntityMapCache { cache := make(EntityMapCache, len(entities)) for _, entity := range entities { + entityID := entity.GetID() + if _, exists := cache[entityID]; exists { + continue + } if entityMap, err := entityToMap(entity.Item()); err == nil { - cache[entity.GetID()] = entityMap + cache[entityID] = entityMap } } return cache diff --git a/apps/workspace-engine/pkg/workspace/relationships/matcher_test.go b/apps/workspace-engine/pkg/workspace/relationships/matcher_test.go index 8a2d6a434..952799e60 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/matcher_test.go +++ b/apps/workspace-engine/pkg/workspace/relationships/matcher_test.go @@ -58,6 +58,32 @@ func TestNewPropertyMatcher(t *testing.T) { } } +func TestBuildEntityMapCache_DedupesIDs(t *testing.T) { + resource1 := &oapi.Resource{ + Id: "resource-1", + Name: "Resource One", + WorkspaceId: "workspace-1", + } + resource2 := &oapi.Resource{ + Id: "resource-1", + Name: "Resource Two", + WorkspaceId: "workspace-1", + } + + entities := []*oapi.RelatableEntity{ + NewResourceEntity(resource1), + NewResourceEntity(resource2), + } + + cache := BuildEntityMapCache(entities) + if len(cache) != 1 { + t.Fatalf("expected cache to dedupe IDs, got %d entries", len(cache)) + } + if _, ok := cache["resource-1"]; !ok { + t.Fatal("expected cache to contain resource-1") + } +} + // TestPropertyMatcher_Evaluate_Equals tests the equals operator func TestPropertyMatcher_Evaluate_Equals(t *testing.T) { tests := []struct {