Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,6 @@ func TestFindRemovedRelations_NoneRemoved(t *testing.T) {

assert.Empty(t, removedRelations)
}

func TestFilterEntitiesByTypeAndSelector(t *testing.T) {
ctx := context.Background()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 7 additions & 3 deletions apps/workspace-engine/pkg/workspace/relationships/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions apps/workspace-engine/pkg/workspace/relationships/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading