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  }