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
207 changes: 85 additions & 122 deletions algo/reach.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package algo

import (
"context"
"sync"
"sync/atomic"

"github.com/gammazero/deque"
"github.com/specterops/dawgs/cache"
"github.com/specterops/dawgs/cardinality"
"github.com/specterops/dawgs/container"
"github.com/specterops/dawgs/graph"
Expand Down Expand Up @@ -42,40 +41,23 @@ type ReachabilityCacheStats struct {
Hits uint64
}

type reachabilityCacheLookup struct {
ComponentReach cardinality.Duplex[uint64]
MemberReach cardinality.Duplex[uint64]
}

type ReachabilityCache struct {
Components ComponentGraph
inboundComponentReach map[uint64]cardinality.Duplex[uint64]
outboundComponentReach map[uint64]cardinality.Duplex[uint64]
inboundMemberReach map[uint64]cardinality.Duplex[uint64]
outboundMemberReach map[uint64]cardinality.Duplex[uint64]
cacheHits *atomic.Uint64
maxCacheSize int
resolved cardinality.Duplex[uint64]
cacheLock *sync.RWMutex
components ComponentGraph
inboundComponentReach cache.Cache[uint64, cardinality.Duplex[uint64]]
outboundComponentReach cache.Cache[uint64, cardinality.Duplex[uint64]]
}

func NewReachabilityCache(ctx context.Context, digraph container.DirectedGraph, maxCacheSize int) *ReachabilityCache {
return &ReachabilityCache{
Components: NewComponentGraph(ctx, digraph),
inboundComponentReach: make(map[uint64]cardinality.Duplex[uint64]),
inboundMemberReach: make(map[uint64]cardinality.Duplex[uint64]),
outboundComponentReach: make(map[uint64]cardinality.Duplex[uint64]),
outboundMemberReach: make(map[uint64]cardinality.Duplex[uint64]),
cacheHits: &atomic.Uint64{},
maxCacheSize: maxCacheSize,
resolved: cardinality.NewBitmap64(),
cacheLock: &sync.RWMutex{},
components: NewComponentGraph(ctx, digraph),
inboundComponentReach: cache.NewSieve[uint64, cardinality.Duplex[uint64]](maxCacheSize),
outboundComponentReach: cache.NewSieve[uint64, cardinality.Duplex[uint64]](maxCacheSize),
}
}

func (s *ReachabilityCache) newReachCursor(component uint64, direction graph.Direction, previous *reachCursor) *reachCursor {
var (
adjacentComponents = container.AdjacentNodes(s.Components.Digraph(), component, direction)
adjacentComponents = container.AdjacentNodes(s.components.Digraph(), component, direction)
componentReach = cardinality.NewBitmap64With(append(adjacentComponents, component)...)
)

Expand All @@ -90,8 +72,8 @@ func (s *ReachabilityCache) newReachCursor(component uint64, direction graph.Dir

func (s *ReachabilityCache) newRootReachCursor(component uint64, direction graph.Direction) *reachCursor {
var (
adjacentComponents = container.AdjacentNodes(s.Components.Digraph(), component, direction)
componentReach = cardinality.NewBitmap64With(append(adjacentComponents, component)...)
adjacentComponents = container.AdjacentNodes(s.components.Digraph(), component, direction)
componentReach = cardinality.NewBitmap64With(component)
)

return &reachCursor{
Expand All @@ -102,147 +84,128 @@ func (s *ReachabilityCache) newRootReachCursor(component uint64, direction graph
}
}

func (s *ReachabilityCache) Stats() ReachabilityCacheStats {
s.cacheLock.RLock()
defer s.cacheLock.RUnlock()

return ReachabilityCacheStats{
Cached: s.resolved.Cardinality(),
Hits: s.cacheHits.Load(),
}
func (s *ReachabilityCache) Stats() cache.Stats {
return s.inboundComponentReach.Stats().Combined(s.outboundComponentReach.Stats())
}

func (s *ReachabilityCache) CanReach(startID, endID uint64, direction graph.Direction) bool {
var (
startComponent, hasStart = s.Components.ContainingComponent(startID)
endComponent, hasEnd = s.Components.ContainingComponent(endID)
startComponent, hasStart = s.components.ContainingComponent(startID)
endComponent, hasEnd = s.components.ContainingComponent(endID)
)

if hasStart && hasEnd {
return s.Components.ComponentReachable(startComponent, endComponent, direction)
return s.components.ComponentReachable(startComponent, endComponent, direction)
}

return false
}

func (s *ReachabilityCache) canCacheComponent(component uint64) bool {
s.cacheLock.RLock()
defer s.cacheLock.RUnlock()
func (s *ReachabilityCache) componentReachToMemberReachBitmap(componentReach cardinality.Duplex[uint64]) cardinality.Duplex[uint64] {
componentMembers := cardinality.NewBitmap64()

return !s.resolved.Contains(component) && len(s.inboundComponentReach)+len(s.outboundComponentReach) < s.maxCacheSize
}
componentReach.Each(func(reachableComponent uint64) bool {
s.components.CollectComponentMembers(reachableComponent, componentMembers)
return true
})

func (s *ReachabilityCache) cacheComponentReach(component uint64, direction graph.Direction, reach cardinality.Duplex[uint64]) {
if !s.canCacheComponent(component) {
return
}
return componentMembers
}

// Collect the component members outside of the lock
componentMembers := cardinality.NewBitmap64()
func (s *ReachabilityCache) componentReachToMemberReachSlice(componentReach cardinality.Duplex[uint64]) []cardinality.Duplex[uint64] {
componentMembers := make([]cardinality.Duplex[uint64], 0, componentReach.Cardinality())

reach.Each(func(reachableComponent uint64) bool {
s.Components.CollectComponentMembers(reachableComponent, componentMembers)
componentReach.Each(func(reachableComponent uint64) bool {
componentMembers = append(componentMembers, s.components.ComponentMembers(reachableComponent))
return true
})

// Lock the cache to save the updated component reach and component member reach
s.cacheLock.Lock()
defer s.cacheLock.Unlock()
return componentMembers
}

if s.resolved.CheckedAdd(component) {
switch direction {
case graph.DirectionInbound:
s.inboundComponentReach[component] = reach
s.inboundMemberReach[component] = componentMembers
func (s *ReachabilityCache) cacheComponentReach(cursor *reachCursor, direction graph.Direction) {
switch direction {
case graph.DirectionInbound:
s.inboundComponentReach.Put(cursor.component, cursor.reach)

case graph.DirectionOutbound:
s.outboundComponentReach[component] = reach
s.outboundMemberReach[component] = componentMembers
}
case graph.DirectionOutbound:
s.outboundComponentReach.Put(cursor.component, cursor.reach)
}
}

func (s *ReachabilityCache) cachedComponentReach(component uint64, direction graph.Direction) (reachabilityCacheLookup, bool) {
// Take the read lock to do a contains check for this component's cached reach
s.cacheLock.RLock()
defer s.cacheLock.RUnlock()

if s.resolved.Contains(component) {
var (
cachedComponentReach cardinality.Duplex[uint64]
cachedMemberReach cardinality.Duplex[uint64]
componentReachCached bool
)

switch direction {
case graph.DirectionInbound:
cachedComponentReach, componentReachCached = s.inboundComponentReach[component]
cachedMemberReach = s.inboundMemberReach[component]

case graph.DirectionOutbound:
cachedComponentReach, componentReachCached = s.outboundComponentReach[component]
cachedMemberReach = s.outboundMemberReach[component]
}
func (s *ReachabilityCache) cachedComponentReach(component uint64, direction graph.Direction) (cardinality.Duplex[uint64], bool) {
var (
entry cardinality.Duplex[uint64]
found = false
)

if componentReachCached {
s.cacheHits.Add(1)
switch direction {
case graph.DirectionInbound:
entry, found = s.inboundComponentReach.Get(component)

return reachabilityCacheLookup{
ComponentReach: cachedComponentReach,
MemberReach: cachedMemberReach,
}, true
}
case graph.DirectionOutbound:
entry, found = s.outboundComponentReach.Get(component)
}

return reachabilityCacheLookup{}, false
return entry, found
}

func (s *ReachabilityCache) componentMemberReachDFS(component uint64, direction graph.Direction) {
func (s *ReachabilityCache) componentReachDFS(component uint64, direction graph.Direction) cardinality.Duplex[uint64] {
if cachedReach, cached := s.cachedComponentReach(component, direction); cached {
return cachedReach
}

var (
stack deque.Deque[*reachCursor]
visitedComponents = cardinality.NewBitmap64()
stack deque.Deque[*reachCursor]
rootCursor = s.newRootReachCursor(component, direction)
)

// Mark the root component as visited and add it to the stack
visitedComponents.Add(component)
stack.PushBack(s.newRootReachCursor(component, direction))
stack.PushBack(rootCursor)

for stack.Len() > 0 {
reachCursor := stack.Back()
nextCursor := stack.Back()

if nextAdjacent, hasNext := reachCursor.NextAdjacent(); !hasNext {
if nextAdjacentComponent, hasNext := nextCursor.NextAdjacent(); !hasNext {
stack.PopBack()

// Complete the cursor to roll up reach cardinalities
reachCursor.Complete()
nextCursor.Complete()

// Update the cache with this component's reach
s.cacheComponentReach(reachCursor.component, direction, reachCursor.reach)
} else if visitedComponents.CheckedAdd(nextAdjacent) {
if cachedReach, cached := s.cachedComponentReach(nextAdjacent, direction); cached {
visitedComponents.Or(cachedReach.ComponentReach)

if reachCursor.reach != visitedComponents {
reachCursor.reach.Or(cachedReach.ComponentReach)
}
s.cacheComponentReach(nextCursor, direction)
} else if rootCursor.reach.CheckedAdd(nextAdjacentComponent) {
// This is a component not yet visited, check if it is cached. If it
// is cached, Or(...) its reach and if not traverse into it.
if cachedReach, cached := s.cachedComponentReach(nextAdjacentComponent, direction); cached {
nextCursor.reach.Or(cachedReach)
} else {
stack.PushBack(s.newReachCursor(nextAdjacent, direction, reachCursor))
stack.PushBack(s.newReachCursor(nextAdjacentComponent, direction, nextCursor))
}
}
}

return rootCursor.reach
}

// ReachSliceOfComponentContainingMember returns the reach of the component containing the given member and direction as
// a slice of membership bitmaps. These bitmaps are not combined for the purposes of maintaining performance when dealing
// with large scale digraphs. The computation cost of producing a single bitmap far exceeds the cost of iteratively
// scanning the returned slice of bitmaps. Additionally, the returned slice may be used by commutative bitmap operations.
func (s *ReachabilityCache) ReachSliceOfComponentContainingMember(member uint64, direction graph.Direction) []cardinality.Duplex[uint64] {
if rootComponent, rootInComponent := s.components.ContainingComponent(member); rootInComponent {
return s.componentReachToMemberReachSlice(s.componentReachDFS(rootComponent, direction))
}

return nil
}

// ReachOfComponentContainingMember returns the reach of the component containing the given member and direction as a single
// bitwise ORed bitmap. For large scale digraphs use of this function may come at a high computational cost. If this function
// is utilized in a tight loop, consider utilizing ReachSliceOfComponentContainingMember with commutative bitmap opreations.
func (s *ReachabilityCache) ReachOfComponentContainingMember(member uint64, direction graph.Direction) cardinality.Duplex[uint64] {
Comment on lines +203 to 206
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in documentation.

Line 205: "opreations" should be "operations".

📝 Proposed fix
 // ReachOfComponentContainingMember returns the reach of the component containing the given member and direction as a single
-// bitwise ORed bitmap. For large scale digraphs use of this function may come at a high computational cost. If this function
-// is utilized in a tight loop, consider utilizing ReachSliceOfComponentContainingMember with commutative bitmap opreations.
+// bitwise ORed bitmap. For large scale digraphs use of this function may come at a high computational cost. If this function
+// is utilized in a tight loop, consider utilizing ReachSliceOfComponentContainingMember with commutative bitmap operations.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ReachOfComponentContainingMember returns the reach of the component containing the given member and direction as a single
// bitwise ORed bitmap. For large scale digraphs use of this function may come at a high computational cost. If this function
// is utilized in a tight loop, consider utilizing ReachSliceOfComponentContainingMember with commutative bitmap opreations.
func (s *ReachabilityCache) ReachOfComponentContainingMember(member uint64, direction graph.Direction) cardinality.Duplex[uint64] {
// ReachOfComponentContainingMember returns the reach of the component containing the given member and direction as a single
// bitwise ORed bitmap. For large scale digraphs use of this function may come at a high computational cost. If this function
// is utilized in a tight loop, consider utilizing ReachSliceOfComponentContainingMember with commutative bitmap operations.
func (s *ReachabilityCache) ReachOfComponentContainingMember(member uint64, direction graph.Direction) cardinality.Duplex[uint64] {
🤖 Prompt for AI Agents
In `@algo/reach.go` around lines 203 - 206, Fix the typo in the doc comment for
ReachOfComponentContainingMember: change "opreations" to "operations" in the
sentence that references ReachSliceOfComponentContainingMember and commutative
bitmap operations so the documentation reads correctly.

if rootComponent, rootInComponent := s.Components.ContainingComponent(member); rootInComponent {
if cachedReach, cached := s.cachedComponentReach(rootComponent, direction); cached {
return cachedReach.MemberReach
} else {
s.componentMemberReachDFS(rootComponent, direction)

if cachedReach, cached := s.cachedComponentReach(rootComponent, direction); cached {
return cachedReach.MemberReach
}
}
if rootComponent, rootInComponent := s.components.ContainingComponent(member); rootInComponent {
return s.componentReachToMemberReachBitmap(s.componentReachDFS(rootComponent, direction))
}

return cardinality.NewBitmap64()
Expand Down Expand Up @@ -270,8 +233,8 @@ func FetchReachabilityCache(ctx context.Context, db graph.Database, criteria gra
if digraph, err := container.FetchDirectedGraph(ctx, db, criteria); err != nil {
return nil, err
} else {
// TODO: Present a more sane config here
return NewReachabilityCache(ctx, digraph, 1_700_000), nil
maxCacheCap := int(float64(digraph.NumNodes()) * .15)
return NewReachabilityCache(ctx, digraph, maxCacheCap), nil
}
}

Expand Down
5 changes: 5 additions & 0 deletions algo/scc.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func (s ComponentGraph) HasMember(memberID uint64) bool {
_, hasMember := s.memberComponentLookup[memberID]
return hasMember
}

func (s ComponentGraph) KnownMembers() cardinality.Duplex[uint64] {
members := cardinality.NewBitmap64()

Expand All @@ -155,6 +156,10 @@ func (s ComponentGraph) ContainingComponent(memberID uint64) (uint64, bool) {
return component, inComponentDigraph
}

func (s ComponentGraph) ComponentMembers(componentID uint64) cardinality.Duplex[uint64] {
return s.componentMembers[componentID]
}

func (s ComponentGraph) CollectComponentMembers(componentID uint64, members cardinality.Duplex[uint64]) {
members.Or(s.componentMembers[componentID])
}
Expand Down
Loading