Optimization for non-cross geofence detection

This commit fixes a performance issue with the algorithm that
determines with geofences are potential candidates for
notifications following a SET operation.

Details

Prior to commit b471873 (10 commits ago) there was a bug where
the "cross" detection was not firing in all cases. This happened
because when looking for candidates for "cross" due to a SET
operation, only the geofences that overlapped the previous
position of the object and the geofences that overlapped the new
position where searched. But, in fac, all of the geofences that
overlapped the union rectangle of the old and new position should
have been searched.

That commit fixed the problem by searching a union rect of the
old and new positions. While this is an accurate solution, it
caused a slowdown on systems that have big/wild position changes
that might cross a huge number of geofences, even when those
geofences did not need actually need "cross" detection.

The fix

With this commit the geofences that have a "cross" detection
are stored in a seperated tree from those that do not. This
allows for a hybrid of the functionality prior and post b471873.

Fixes #583
This commit is contained in:
tidwall 2020-10-23 09:51:27 -07:00
parent 100be7be3c
commit 9998e03f6f
4 changed files with 87 additions and 47 deletions

View File

@ -15,7 +15,6 @@ import (
"time" "time"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/redcon" "github.com/tidwall/redcon"
"github.com/tidwall/resp" "github.com/tidwall/resp"
@ -227,55 +226,70 @@ func (s *Server) writeAOF(args []string, d *commandDetails) error {
} }
func (s *Server) getQueueCandidates(d *commandDetails) []*Hook { func (s *Server) getQueueCandidates(d *commandDetails) []*Hook {
var candidates []*Hook candidates := make(map[*Hook]bool)
// add the hooks with "outside" detection // add the hooks with "outside" detection
if len(s.hooksOut) > 0 { for _, hook := range s.hooksOut {
for _, hook := range s.hooksOut { if hook.Key == d.key {
if hook.Key == d.key { candidates[hook] = true
candidates = append(candidates, hook)
}
} }
} }
// search the hook spatial tree // look for candidates that might "cross" geofences
// build a rectangle that fills the old and new which will be enough to if d.oldObj != nil && d.obj != nil && s.hookCross.Len() > 0 {
// handle "enter", "inside", "exit", and "cross" detections. r1, r2 := d.oldObj.Rect(), d.obj.Rect()
var rect geometry.Rect s.hookCross.Search(
if d.oldObj != nil { [2]float64{
rect = d.oldObj.Rect() math.Min(r1.Min.X, r2.Min.X),
if d.obj != nil { math.Min(r1.Min.Y, r2.Min.Y),
r2 := d.obj.Rect() },
rect.Min.X = math.Min(rect.Min.X, r2.Min.X) [2]float64{
rect.Min.Y = math.Min(rect.Min.Y, r2.Min.Y) math.Max(r1.Max.X, r2.Max.X),
rect.Max.X = math.Max(rect.Max.X, r2.Max.X) math.Max(r1.Max.Y, r2.Max.Y),
rect.Max.Y = math.Max(rect.Max.Y, r2.Max.Y) },
} func(min, max [2]float64, value interface{}) bool {
} else if d.obj != nil { hook := value.(*Hook)
rect = d.obj.Rect() if hook.Key == d.key {
} else { candidates[hook] = true
return candidates
}
s.hookTree.Search(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
func(_, _ [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key != d.key {
return true
}
var found bool
for _, candidate := range candidates {
if candidate == hook {
found = true
break
} }
} return true
if !found { })
candidates = append(candidates, hook) }
} // look for candidates that overlap the old object
return true if d.oldObj != nil {
}, r1 := d.oldObj.Rect()
) s.hookTree.Search(
return candidates [2]float64{r1.Min.X, r1.Min.Y},
[2]float64{r1.Max.X, r1.Max.Y},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
// look for candidates that overlap the new object
if d.obj != nil {
r1 := d.obj.Rect()
s.hookTree.Search(
[2]float64{r1.Min.X, r1.Min.Y},
[2]float64{r1.Max.X, r1.Max.Y},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
if len(candidates) == 0 {
return nil
}
// return the candidates as a slice
ret := make([]*Hook, 0, len(candidates))
for hook := range candidates {
ret = append(ret, hook)
}
return ret
} }
func (s *Server) queueHooks(d *commandDetails) error { func (s *Server) queueHooks(d *commandDetails) error {

View File

@ -514,6 +514,7 @@ func (server *Server) cmdFlushDB(msg *Message) (res resp.Value, d commandDetails
server.hooks = make(map[string]*Hook) server.hooks = make(map[string]*Hook)
server.hooksOut = make(map[string]*Hook) server.hooksOut = make(map[string]*Hook)
server.hookTree = rbang.RTree{} server.hookTree = rbang.RTree{}
server.hookCross = rbang.RTree{}
d.command = "flushdb" d.command = "flushdb"
d.updated = true d.updated = true
d.timestamp = time.Now() d.timestamp = time.Now()

View File

@ -197,6 +197,12 @@ func (s *Server) cmdSetHook(msg *Message, chanCmd bool) (
[2]float64{rect.Min.X, rect.Min.Y}, [2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y}, [2]float64{rect.Max.X, rect.Max.Y},
prevHook) prevHook)
if prevHook.Fence.detect["cross"] {
s.hookCross.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
prevHook)
}
} }
// add hook to spatial index // add hook to spatial index
if hook != nil && hook.Fence != nil && hook.Fence.obj != nil { if hook != nil && hook.Fence != nil && hook.Fence.obj != nil {
@ -205,6 +211,12 @@ func (s *Server) cmdSetHook(msg *Message, chanCmd bool) (
[2]float64{rect.Min.X, rect.Min.Y}, [2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y}, [2]float64{rect.Max.X, rect.Max.Y},
hook) hook)
if hook.Fence.detect["cross"] {
s.hookCross.Insert(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
}
} }
hook.Open() // Opens a goroutine to notify the hook hook.Open() // Opens a goroutine to notify the hook
@ -246,6 +258,12 @@ func (s *Server) cmdDelHook(msg *Message, chanCmd bool) (
[2]float64{rect.Min.X, rect.Min.Y}, [2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y}, [2]float64{rect.Max.X, rect.Max.Y},
hook) hook)
if hook.Fence.detect["cross"] {
s.hookCross.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
}
} }
d.updated = true d.updated = true
} }
@ -298,6 +316,12 @@ func (s *Server) cmdPDelHook(msg *Message, channel bool) (
[2]float64{rect.Min.X, rect.Min.Y}, [2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y}, [2]float64{rect.Max.X, rect.Max.Y},
hook) hook)
if hook.Fence.detect["cross"] {
s.hookCross.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
}
} }
d.updated = true d.updated = true
count++ count++

View File

@ -115,7 +115,8 @@ type Server struct {
shrinking bool // aof shrinking flag shrinking bool // aof shrinking flag
shrinklog [][]string // aof shrinking log shrinklog [][]string // aof shrinking log
hooks map[string]*Hook // hook name hooks map[string]*Hook // hook name
hookTree rbang.RTree // hook spatial tree containing all hookCross rbang.RTree // hook spatial tree for "cross" geofences
hookTree rbang.RTree // hook spatial tree for all
hooksOut map[string]*Hook // hooks with "outside" detection hooksOut map[string]*Hook // hooks with "outside" detection
aofconnM map[net.Conn]bool aofconnM map[net.Conn]bool
luascripts *lScriptMap luascripts *lScriptMap