go.ligato.io/vpp-agent/v3@v3.5.0/plugins/kvscheduler/internal/graph/edge_lookup.go (about) 1 // Copyright (c) 2019 Cisco and/or its affiliates. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at: 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package graph 16 17 import ( 18 "fmt" 19 "reflect" 20 "sort" 21 "strings" 22 ) 23 24 const ( 25 initNodeKeysCap = 1000 26 initEdgesCap = 10000 27 initDirDepthBoundsCap = 10 28 29 dirSeparator = "/" 30 ) 31 32 // edgeLookup is a helper tool used internally by kvgraph for **efficient** lookups 33 // over the set of graph edges, defined using keys or key prefixes. 34 type edgeLookup struct { 35 nodeKeyData []nodeKey 36 nodeKeyOffset []int // for O(log(n)) lookups against key prefixes 37 nodeKeyMap map[string]bool // for O(1) lookups against full keys 38 removedNodes int 39 40 edgeData []edge // unordered 41 edgeOffset []int // ordered first by directory depth, then lexicographically by edge data 42 removedEdges int 43 // edges are first sorted (split) by the number of target key components (directories) 44 // -> dirDepthBounds[dirCount] = index of the first edge in <edgeOffset> whose 45 // targetKey consists of <dirCount> directories (incl. the last suffix) 46 dirDepthBounds []int 47 48 overlay *edgeLookup 49 underlay *edgeLookup 50 51 methodTracker MethodTracker 52 } 53 54 type nodeKey struct { 55 key string 56 removed bool 57 58 decOffset int // used internally in gcNodeKeys() 59 } 60 61 type edge struct { 62 // can be empty prefix to match all 63 targetKey string 64 isPrefix bool 65 66 sourceNode string 67 relation string 68 label string 69 70 removed bool 71 72 decOffset int // used internally in gcEdges() 73 } 74 75 func newEdgeLookup(mt MethodTracker) *edgeLookup { 76 return &edgeLookup{ 77 nodeKeyData: make([]nodeKey, 0, initNodeKeysCap), 78 nodeKeyOffset: make([]int, 0, initNodeKeysCap), 79 nodeKeyMap: make(map[string]bool), 80 edgeData: make([]edge, 0, initEdgesCap), 81 edgeOffset: make([]int, 0, initEdgesCap), 82 dirDepthBounds: make([]int, 0, initDirDepthBoundsCap), 83 methodTracker: mt, 84 } 85 } 86 87 func (el *edgeLookup) reset() { 88 el.nodeKeyMap = make(map[string]bool) 89 el.nodeKeyData = el.nodeKeyData[:0] 90 el.nodeKeyOffset = el.nodeKeyOffset[:0] 91 el.removedNodes = 0 92 el.edgeData = el.edgeData[:0] 93 el.edgeOffset = el.edgeOffset[:0] 94 el.dirDepthBounds = el.dirDepthBounds[:0] 95 el.removedEdges = 0 96 } 97 98 func (el *edgeLookup) makeOverlay() *edgeLookup { 99 if el.overlay == nil { 100 // create overlay for the first time 101 el.overlay = &edgeLookup{ 102 nodeKeyData: make([]nodeKey, 0, max(len(el.nodeKeyData), initNodeKeysCap)), 103 nodeKeyOffset: make([]int, 0, max(len(el.nodeKeyOffset), initNodeKeysCap)), 104 edgeData: make([]edge, 0, max(len(el.edgeData), initEdgesCap)), 105 edgeOffset: make([]int, 0, max(len(el.edgeOffset), initEdgesCap)), 106 dirDepthBounds: make([]int, 0, max(len(el.dirDepthBounds), initDirDepthBoundsCap)), 107 underlay: el, 108 } 109 } 110 // re-use previously allocated memory 111 el.overlay.resizeNodeKeys(len(el.nodeKeyOffset)) 112 el.overlay.resizeEdges(len(el.edgeOffset)) 113 el.overlay.resizeDirDepthBounds(len(el.dirDepthBounds)) 114 copy(el.overlay.nodeKeyData, el.nodeKeyData) 115 copy(el.overlay.nodeKeyOffset, el.nodeKeyOffset) 116 copy(el.overlay.edgeData, el.edgeData) 117 copy(el.overlay.edgeOffset, el.edgeOffset) 118 copy(el.overlay.dirDepthBounds, el.dirDepthBounds) 119 el.overlay.nodeKeyMap = make(map[string]bool) 120 el.overlay.removedEdges = el.removedEdges 121 el.overlay.removedNodes = el.removedNodes 122 return el.overlay 123 } 124 125 func (el *edgeLookup) saveOverlay() { 126 if el.underlay == nil { 127 panic("called saveOverlay on what is not overlay") 128 } 129 el.underlay.removedNodes = el.removedNodes 130 el.underlay.removedEdges = el.removedEdges 131 for key, add := range el.nodeKeyMap { 132 if add { 133 el.underlay.nodeKeyMap[key] = true 134 } else { 135 delete(el.underlay.nodeKeyMap, key) 136 } 137 } 138 el.nodeKeyMap = make(map[string]bool) // clear 139 el.underlay.resizeNodeKeys(len(el.nodeKeyOffset)) 140 el.underlay.resizeEdges(len(el.edgeOffset)) 141 el.underlay.resizeDirDepthBounds(len(el.dirDepthBounds)) 142 copy(el.underlay.nodeKeyData, el.nodeKeyData) 143 copy(el.underlay.nodeKeyOffset, el.nodeKeyOffset) 144 copy(el.underlay.edgeData, el.edgeData) 145 copy(el.underlay.edgeOffset, el.edgeOffset) 146 copy(el.underlay.dirDepthBounds, el.dirDepthBounds) 147 } 148 149 // O(log(n)) 150 func (el *edgeLookup) addNodeKey(key string) { 151 if el.methodTracker != nil { 152 defer el.methodTracker("edgeLookup.addNodeKey")() 153 } 154 155 el.nodeKeyMap[key] = true 156 157 // find the corresponding index in nodeKeyOffset 158 idx := el.nodeKeyIdx(key) 159 if idx < len(el.nodeKeyOffset) { 160 offset := el.nodeKeyOffset[idx] 161 if el.nodeKeyData[offset].key == key { 162 if el.nodeKeyData[offset].removed { 163 el.nodeKeyData[offset].removed = false 164 el.removedNodes-- 165 } 166 return 167 } 168 } 169 170 // add to both nodeKeyOffset and nodeKeyData 171 el.nodeKeyData = append(el.nodeKeyData, nodeKey{}) 172 offset := len(el.nodeKeyData) - 1 173 el.nodeKeyData[offset].key = key 174 el.nodeKeyData[offset].removed = false 175 el.nodeKeyOffset = append(el.nodeKeyOffset, -1) 176 if idx < len(el.nodeKeyOffset)-1 { 177 copy(el.nodeKeyOffset[idx+1:], el.nodeKeyOffset[idx:]) 178 } 179 el.nodeKeyOffset[idx] = offset 180 } 181 182 // O(log(n)) amortized 183 func (el *edgeLookup) delNodeKey(key string) { 184 if el.methodTracker != nil { 185 defer el.methodTracker("edgeLookup.delNodeKey")() 186 } 187 188 if el.underlay != nil { 189 // this is overlay, remember operation 190 el.nodeKeyMap[key] = false 191 } else { 192 // do not store false, otherwise memory usage will grow 193 delete(el.nodeKeyMap, key) 194 } 195 idx := el.nodeKeyIdx(key) 196 if idx < len(el.nodeKeyOffset) { 197 offset := el.nodeKeyOffset[idx] 198 if el.nodeKeyData[offset].key == key && !el.nodeKeyData[offset].removed { 199 el.nodeKeyData[offset].removed = true 200 el.removedNodes++ 201 if el.removedNodes > len(el.nodeKeyData)/2 { 202 el.gcNodeKeys() 203 } 204 } 205 } 206 } 207 208 // O(log(n)) 209 func (el *edgeLookup) nodeKeyIdx(key string) int { 210 return sort.Search(len(el.nodeKeyOffset), 211 func(i int) bool { 212 return key <= el.nodeKeyData[el.nodeKeyOffset[i]].key 213 }) 214 } 215 216 // O(log(m)) 217 func (el *edgeLookup) addEdge(e edge) { 218 if el.methodTracker != nil { 219 defer el.methodTracker("edgeLookup.addEdge")() 220 } 221 222 // find the corresponding index in edgeOffset 223 e.targetKey = trimTrailingDirSep(e.targetKey) 224 dirDepth := getDirDepth(e.targetKey) 225 idx := el.edgeIdx(e, dirDepth) 226 if idx < len(el.edgeOffset) { 227 offset := el.edgeOffset[idx] 228 equal, _ := e.compare(el.edgeData[offset]) 229 if equal { 230 if el.edgeData[offset].removed { 231 el.edgeData[offset].removed = false 232 el.removedEdges-- 233 } 234 return 235 } 236 } 237 238 // add to both edgeOffset and edgeData 239 el.edgeData = append(el.edgeData, e) 240 offset := len(el.edgeData) - 1 241 el.edgeData[offset].removed = false 242 el.edgeOffset = append(el.edgeOffset, -1) 243 if idx < len(el.edgeOffset)-1 { 244 copy(el.edgeOffset[idx+1:], el.edgeOffset[idx:]) 245 } 246 el.edgeOffset[idx] = offset 247 248 // update directory boundaries 249 for i := dirDepth + 1; i < len(el.dirDepthBounds); i++ { 250 el.dirDepthBounds[i]++ 251 } 252 for i := len(el.dirDepthBounds); i <= dirDepth; i++ { 253 el.dirDepthBounds = append(el.dirDepthBounds, len(el.edgeOffset)-1) 254 } 255 } 256 257 // O(log(m)) amortized 258 func (el *edgeLookup) delEdge(e edge) { 259 if el.methodTracker != nil { 260 defer el.methodTracker("edgeLookup.delEdge")() 261 } 262 263 e.targetKey = trimTrailingDirSep(e.targetKey) 264 dirDepth := getDirDepth(e.targetKey) 265 idx := el.edgeIdx(e, dirDepth) 266 if idx < len(el.edgeOffset) { 267 offset := el.edgeOffset[idx] 268 equal, _ := e.compare(el.edgeData[offset]) 269 if equal && !el.edgeData[offset].removed { 270 el.edgeData[offset].removed = true 271 el.removedEdges++ 272 if el.removedEdges > len(el.edgeData)/2 { 273 el.gcEdges() 274 } 275 } 276 } 277 } 278 279 // O(log(m)) 280 func (el *edgeLookup) edgeIdx(e edge, dirDepth int) int { 281 begin, end := el.getDirDepthBounds(dirDepth) 282 if begin == end { 283 return begin 284 } 285 return begin + sort.Search(end-begin, 286 func(i int) bool { 287 e2 := el.edgeData[el.edgeOffset[begin+i]] 288 _, order := e.compare(e2) 289 return order <= 0 290 }) 291 } 292 293 func (el *edgeLookup) getDirDepthBounds(dirDepth int) (begin, end int) { 294 if dirDepth < len(el.dirDepthBounds) { 295 begin = el.dirDepthBounds[dirDepth] 296 } else { 297 begin = len(el.edgeOffset) 298 } 299 if dirDepth < len(el.dirDepthBounds)-1 { 300 end = el.dirDepthBounds[dirDepth+1] 301 } else { 302 end = len(el.edgeOffset) 303 } 304 return 305 } 306 307 // for prefix: O(log(n)) (assuming O(1) matched keys) 308 // for full key: O(1) average, O(n) worst-case 309 func (el *edgeLookup) iterTargets(key string, isPrefix bool, cb func(targetNode string)) { 310 if el.methodTracker != nil { 311 defer el.methodTracker("edgeLookup.iterTargets")() 312 } 313 314 if key == "" && isPrefix { 315 // iterate all 316 for i := range el.nodeKeyData { 317 if el.nodeKeyData[i].removed { 318 continue 319 } 320 cb(el.nodeKeyData[i].key) 321 } 322 return 323 } 324 if !isPrefix { 325 added, known := el.nodeKeyMap[key] 326 if (known && !added) || (!known && el.underlay == nil) { 327 return 328 } 329 if !known && el.underlay != nil { 330 _, added = el.underlay.nodeKeyMap[key] 331 if !added { 332 return 333 } 334 } 335 cb(key) 336 return 337 } 338 // prefix: 339 idx := el.nodeKeyIdx(key) 340 for i := idx; i < len(el.nodeKeyOffset); i++ { 341 offset := el.nodeKeyOffset[i] 342 if el.nodeKeyData[offset].removed { 343 continue 344 } 345 if !strings.HasPrefix(el.nodeKeyData[offset].key, key) { 346 break 347 } 348 cb(el.nodeKeyData[offset].key) 349 } 350 } 351 352 // O(log(m)) (assuming O(1) matched sources) 353 func (el *edgeLookup) iterSources(targetKey string, cb func(sourceNode, relation, label string)) { 354 if el.methodTracker != nil { 355 defer el.methodTracker("edgeLookup.iterSources")() 356 } 357 targetKey = trimTrailingDirSep(targetKey) 358 359 var dirDepth int 360 for i := 0; i <= len(targetKey); i++ { 361 prefix := i < len(targetKey) 362 if i == 0 || !prefix || targetKey[i] == dirSeparator[0] { 363 idx := el.edgeIdx(edge{targetKey: targetKey[:i]}, dirDepth) 364 _, end := el.getDirDepthBounds(dirDepth) 365 for j := idx; j < end; j++ { 366 offset := el.edgeOffset[j] 367 if el.edgeData[offset].targetKey != targetKey[:i] { 368 break 369 } 370 if prefix && !el.edgeData[offset].isPrefix { 371 continue 372 } 373 if el.edgeData[offset].removed { 374 continue 375 } 376 cb(el.edgeData[offset].sourceNode, el.edgeData[offset].relation, el.edgeData[offset].label) 377 } 378 dirDepth++ 379 } 380 } 381 } 382 383 // O(n) 384 func (el *edgeLookup) gcNodeKeys() { 385 // for each offset determine how much it will decrease 386 var decOffset int 387 for i := 0; i < len(el.nodeKeyData); i++ { 388 if el.nodeKeyData[i].removed { 389 decOffset++ 390 } else { 391 el.nodeKeyData[i].decOffset = decOffset 392 } 393 } 394 395 // GC node-key offsets 396 var next int 397 for i := range el.nodeKeyOffset { 398 offset := el.nodeKeyOffset[i] 399 if !el.nodeKeyData[offset].removed { 400 if next < i { 401 el.nodeKeyOffset[next] = el.nodeKeyOffset[i] 402 } 403 el.nodeKeyOffset[next] -= el.nodeKeyData[offset].decOffset 404 next++ 405 } 406 } 407 el.nodeKeyOffset = el.nodeKeyOffset[:next] 408 409 // GC node-key data 410 next = 0 411 for i := 0; i < len(el.nodeKeyData); i++ { 412 if !el.nodeKeyData[i].removed { 413 if next < i { 414 el.nodeKeyData[next] = el.nodeKeyData[i] 415 } 416 next++ 417 } 418 } 419 el.nodeKeyData = el.nodeKeyData[:next] 420 421 el.removedNodes = 0 422 if len(el.nodeKeyOffset) != len(el.nodeKeyData) { 423 panic("len(el.nodeKeyOffset) != len(el.nodeKeyData)") 424 } 425 } 426 427 // O(m) 428 func (el *edgeLookup) gcEdges() { 429 // for each offset determine how much it will decrease 430 var decOffset int 431 for i := 0; i < len(el.edgeData); i++ { 432 if el.edgeData[i].removed { 433 decOffset++ 434 } else { 435 el.edgeData[i].decOffset = decOffset 436 } 437 } 438 439 // GC edge offsets 440 var next int 441 for dIdx, curBound := range el.dirDepthBounds { 442 newBound := next 443 nextBound := len(el.edgeOffset) 444 if dIdx < len(el.dirDepthBounds)-1 { 445 nextBound = el.dirDepthBounds[dIdx+1] 446 } 447 for i := curBound; i < nextBound; i++ { 448 offset := el.edgeOffset[i] 449 if !el.edgeData[offset].removed { 450 if next < i { 451 el.edgeOffset[next] = el.edgeOffset[i] 452 } 453 // update offset to reflect the post-GC situation 454 el.edgeOffset[next] -= el.edgeData[offset].decOffset 455 next++ 456 } 457 } 458 el.dirDepthBounds[dIdx] = newBound 459 } 460 el.edgeOffset = el.edgeOffset[:next] 461 462 // GC edge data 463 next = 0 464 for i := 0; i < len(el.edgeData); i++ { 465 if !el.edgeData[i].removed { 466 if next < i { 467 el.edgeData[next] = el.edgeData[i] 468 } 469 next++ 470 } 471 } 472 el.edgeData = el.edgeData[:next] 473 474 // GC directory boundaries 475 dIdx := len(el.dirDepthBounds) - 1 476 for ; dIdx >= 0; dIdx-- { 477 if el.dirDepthBounds[dIdx] < len(el.edgeOffset) { 478 break 479 } 480 } 481 el.dirDepthBounds = el.dirDepthBounds[:dIdx+1] 482 483 el.removedEdges = 0 484 if len(el.edgeOffset) != len(el.edgeData) { 485 panic("len(el.edgeOffset) != len(el.edgeData)") 486 } 487 } 488 489 func (el *edgeLookup) resizeNodeKeys(size int) { 490 if cap(el.nodeKeyData) < size { 491 el.nodeKeyData = make([]nodeKey, size) 492 el.nodeKeyOffset = make([]int, size) 493 } 494 el.nodeKeyData = el.nodeKeyData[0:size] 495 el.nodeKeyOffset = el.nodeKeyOffset[0:size] 496 } 497 498 func (el *edgeLookup) resizeEdges(size int) { 499 if cap(el.edgeData) < size { 500 el.edgeData = make([]edge, size) 501 el.edgeOffset = make([]int, size) 502 } 503 el.edgeData = el.edgeData[0:size] 504 el.edgeOffset = el.edgeOffset[0:size] 505 } 506 507 func (el *edgeLookup) resizeDirDepthBounds(size int) { 508 if cap(el.dirDepthBounds) < size { 509 el.dirDepthBounds = make([]int, size) 510 } 511 el.dirDepthBounds = el.dirDepthBounds[0:size] 512 } 513 514 // for UTs 515 func (el *edgeLookup) verifyDirDepthBounds() error { 516 if len(el.edgeData) != len(el.edgeOffset) { 517 return fmt.Errorf("len(edgeData) != len(edgeOffset) (%d != %d)", 518 len(el.edgeData), len(el.edgeOffset)) 519 } 520 if cap(el.edgeData) != cap(el.edgeOffset) { 521 return fmt.Errorf("cap(edgeData) != cap(edgeOffset) (%d != %d)", 522 cap(el.edgeData), cap(el.edgeOffset)) 523 } 524 for i := 0; i < len(el.edgeOffset); i++ { 525 found := false 526 for j := 0; j < len(el.edgeOffset); j++ { 527 if el.edgeOffset[j] == i { 528 found = true 529 break 530 } 531 } 532 if !found { 533 return fmt.Errorf("missing entry for edge offset %d (offsets=%+v)", 534 i, el.edgeOffset) 535 } 536 } 537 538 if len(el.nodeKeyData) != len(el.nodeKeyOffset) { 539 return fmt.Errorf("len(nodeKeyData) != len(nodeKeyOffset) (%d != %d)", 540 len(el.nodeKeyData), len(el.nodeKeyOffset)) 541 } 542 if cap(el.nodeKeyData) != cap(el.nodeKeyOffset) { 543 return fmt.Errorf("cap(nodeKeyData) != cap(nodeKeyOffset) (%d != %d)", 544 cap(el.nodeKeyData), cap(el.nodeKeyOffset)) 545 } 546 for i := 0; i < len(el.nodeKeyOffset); i++ { 547 found := false 548 for j := 0; j < len(el.nodeKeyOffset); j++ { 549 if el.nodeKeyOffset[j] == i { 550 found = true 551 break 552 } 553 } 554 if !found { 555 return fmt.Errorf("missing entry for node-key offset %d (offsets=%+v)", 556 i, el.nodeKeyOffset) 557 } 558 } 559 560 expBounds := []int{} 561 dirDepth := -1 562 for i := range el.edgeOffset { 563 tk := el.edgeData[el.edgeOffset[i]].targetKey 564 if len(tk) > 0 && tk[len(tk)-1] == dirSeparator[0] { 565 return fmt.Errorf("edge with targetKey ending with dir separator: %s", tk) 566 } 567 var tkDirDepth int 568 if tk != "" { 569 tkDirDepth = len(strings.Split(tk, dirSeparator)) 570 } 571 572 if tkDirDepth < dirDepth { 573 return fmt.Errorf("edge with targetKey inserted at a wrong dir depth (%d): %s", 574 dirDepth, tk) 575 } 576 for j := dirDepth + 1; j <= tkDirDepth; j++ { 577 expBounds = append(expBounds, i) 578 } 579 dirDepth = tkDirDepth 580 } 581 // bad performance of this is OK, the method is used only in unit tests 582 if !reflect.DeepEqual(el.dirDepthBounds, expBounds) { 583 return fmt.Errorf("unexpected dir-depth bounds: expected=%v, actual=%v "+ 584 "(offsets=%+v, data=%+v)", 585 expBounds, el.dirDepthBounds, el.edgeOffset, el.edgeData) 586 } 587 return nil 588 } 589 590 func (e edge) compare(e2 edge) (equal bool, order int) { 591 if e.targetKey < e2.targetKey { 592 return false, -1 593 } 594 if e.targetKey > e2.targetKey { 595 return false, 1 596 } 597 if e.isPrefix != e2.isPrefix { 598 if !e.isPrefix { 599 return false, -1 600 } 601 return false, 1 602 } 603 if e.sourceNode < e2.sourceNode { 604 return false, -1 605 } 606 if e.sourceNode > e2.sourceNode { 607 return false, 1 608 } 609 if e.relation < e2.relation { 610 return false, -1 611 } 612 if e.relation > e2.relation { 613 return false, 1 614 } 615 if e.label < e2.label { 616 return false, -1 617 } 618 if e.label > e2.label { 619 return false, 1 620 } 621 return true, 0 622 } 623 624 func max(a, b int) int { 625 if a >= b { 626 return a 627 } 628 return b 629 } 630 631 func trimTrailingDirSep(s string) string { 632 for len(s) > 0 && s[0] == dirSeparator[0] { 633 s = s[1:] 634 } 635 for len(s) > 0 && s[len(s)-1] == dirSeparator[0] { 636 s = s[:len(s)-1] 637 } 638 return s 639 } 640 641 func getDirDepth(s string) int { 642 var depth int 643 if len(s) > 0 { 644 depth++ // include last suffix (assuming no trailing separator) 645 } 646 depth += strings.Count(s, dirSeparator) 647 return depth 648 }