Skip to content
Closed
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
146 changes: 80 additions & 66 deletions vermeer/algorithms/triangle_count.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,96 +47,110 @@ func (tc *TriangleCountMaker) DataNeeded() []string {
return []string{compute.UseOutEdge}
}

// buildTriangleNeighborState treats incoming and outgoing edges as one
// undirected neighborhood, removes self-loops and duplicate parallel edges,
// and returns only the lower-ID half for cross-worker exchange. For every
// triangle incident to vertex v, exactly one of the other two vertices has the
// larger ID, so intersecting N(v) with each neighbor's lower-ID half counts the
// triangle once for v.
func buildTriangleNeighborState(vertexID uint32, inEdges,
outEdges serialize.SliceUint32) (map[serialize.SUint32]struct{}, serialize.SliceUint32) {
neighbors := make(map[serialize.SUint32]struct{}, len(inEdges)+len(outEdges))
lower := make(serialize.SliceUint32, 0, (len(inEdges)+len(outEdges))/2)
add := func(neighbor serialize.SUint32) {
if uint32(neighbor) == vertexID {
return
}
if _, exists := neighbors[neighbor]; exists {
return
}
neighbors[neighbor] = struct{}{}
if uint32(neighbor) < vertexID {
lower = append(lower, neighbor)
}
}
for _, neighbor := range inEdges {
add(neighbor)
}
for _, neighbor := range outEdges {
add(neighbor)
}
return neighbors, lower
}

func countTriangleMembership(neighbors map[serialize.SUint32]struct{},
lowerNeighbors serialize.SliceUint32) serialize.SUint32 {
var count serialize.SUint32
for _, candidate := range lowerNeighbors {
if _, exists := neighbors[candidate]; exists {
Comment on lines +84 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid scanning the larger adjacency for every intersection

For graphs with high-degree hubs, always scanning lowerNeighbors introduces a quadratic runtime regression. Consider a star with n leaves whose hub has the largest vertex ID: each leaf now scans all n entries in the hub's lower-neighbor slice, making step 2 perform Θ(n²) membership checks even though there are no triangles. The previous findCross scanned the smaller set, so the same graph required only Θ(n) checks. Preserve a smaller-side intersection strategy, for example by sorting the exchanged slices and using binary searches when the local neighbor set is smaller, so compact exchange does not make sparse hub-heavy graphs impractical to process.

Useful? React with 👍 / 👎.

count++
}
}
return count
}

type TriangleCountWorker struct {
compute.WorkerComputerBase
triangleCount []serialize.SUint32
totalNeighborSet []serialize.MapUint32Struct
selfNeighborSet []map[serialize.SUint32]struct{}
triangleCount []serialize.SUint32
lowerNeighbors []serialize.SliceUint32
localNeighbors []map[serialize.SUint32]struct{}
}

func (tcw *TriangleCountWorker) VertexValue(i uint32) serialize.MarshalAble {
func (tcw *TriangleCountWorker) VertexValue(vertexID uint32) serialize.MarshalAble {
if tcw.WContext.Output {
return &tcw.triangleCount[vertexID]
}
if tcw.WContext.Step == 1 {
return &tcw.totalNeighborSet[i]
return &tcw.lowerNeighbors[vertexID]
}
return &tcw.triangleCount[i]
return &tcw.triangleCount[vertexID]
}

func (tcw *TriangleCountWorker) Init() error {
tcw.triangleCount = make([]serialize.SUint32, tcw.WContext.GraphData.Vertex.TotalVertexCount())
tcw.totalNeighborSet = make([]serialize.MapUint32Struct, tcw.WContext.GraphData.Vertex.TotalVertexCount())
tcw.selfNeighborSet = make([]map[serialize.SUint32]struct{}, tcw.WContext.GraphData.VertexCount)
totalVertices := tcw.WContext.GraphData.Vertex.TotalVertexCount()
localVertices := tcw.WContext.GraphData.VertexCount
tcw.triangleCount = make([]serialize.SUint32, totalVertices)
// Slice headers are substantially cheaper than one Go map per global
// vertex. Only local vertices retain hash sets for membership tests.
tcw.lowerNeighbors = make([]serialize.SliceUint32, totalVertices)
tcw.localNeighbors = make([]map[serialize.SUint32]struct{}, localVertices)
return nil
}

func (tcw *TriangleCountWorker) Compute(vertexID uint32, pID int) {
_ = pID
vertID := vertexID - tcw.WContext.GraphData.VertIDStart
localID := vertexID - tcw.WContext.GraphData.VertIDStart
switch tcw.WContext.Step {
case 1:
//if tcw.WContext.GraphData.BothEdges != nil {
// tcw.selfNeighborSet[vertID] = make(map[serialize.SUint32]struct{},
// len(tcw.WContext.GraphData.BothEdges[vertID]))
// tcw.totalNeighborSet[vertexID] = make(map[serialize.SUint32]struct{},
// len(tcw.WContext.GraphData.BothEdges[vertID])/2)
// for _, nID := range tcw.WContext.GraphData.BothEdges[vertID] {
// if uint32(nID) == vertexID {
// continue
// }
// tcw.selfNeighborSet[vertID][nID] = struct{}{}
// if uint32(nID) < vertexID {
// tcw.totalNeighborSet[vertexID][nID] = struct{}{}
// }
// }
//} else {
inEdges := tcw.WContext.GraphData.Edges.GetInEdges(vertID)
outEdges := tcw.WContext.GraphData.Edges.GetOutEdges(vertID)
tcw.selfNeighborSet[vertID] = make(map[serialize.SUint32]struct{},
len(inEdges)+len(outEdges))
tcw.totalNeighborSet[vertexID] = make(map[serialize.SUint32]struct{},
(len(inEdges)+len(outEdges))/2)
for _, nID := range inEdges {
if uint32(nID) == vertexID {
continue
}
tcw.selfNeighborSet[vertID][nID] = struct{}{}
if uint32(nID) < vertexID {
tcw.totalNeighborSet[vertexID][nID] = struct{}{}
}
}
for _, nID := range outEdges {
if uint32(nID) == vertexID {
neighbors, lower := buildTriangleNeighborState(vertexID,
tcw.WContext.GraphData.Edges.GetInEdges(localID),
tcw.WContext.GraphData.Edges.GetOutEdges(localID))
tcw.localNeighbors[localID] = neighbors
tcw.lowerNeighbors[vertexID] = lower
case 2:
neighbors := tcw.localNeighbors[localID]
var count serialize.SUint32
for neighborID := range neighbors {
if int(neighborID) >= len(tcw.lowerNeighbors) {
continue
}
tcw.selfNeighborSet[vertID][nID] = struct{}{}
if uint32(nID) < vertexID {
tcw.totalNeighborSet[vertexID][nID] = struct{}{}
}
}
//}
case 2:
for fID := range tcw.selfNeighborSet[vertID] {
tcw.triangleCount[vertexID] += tcw.findCross(tcw.selfNeighborSet[vertID], tcw.totalNeighborSet[fID])
count += countTriangleMembership(neighbors,
tcw.lowerNeighbors[neighborID])
}
tcw.triangleCount[vertexID] = count
}
}

func (tcw *TriangleCountWorker) findCross(a map[serialize.SUint32]struct{}, b map[serialize.SUint32]struct{}) serialize.SUint32 {
var count serialize.SUint32 = 0
if len(a) > len(b) {
for k := range b {
if _, ok := a[k]; ok {
count++
}
}
} else {
for k := range a {
if _, ok := b[k]; ok {
count++
}
}
func (tcw *TriangleCountWorker) AfterStep() {
if tcw.WContext.Step != 2 {
return
}
return count
// Output needs only the final count. Release adjacency workspace before the
// writer starts so a large graph is not held twice during result export.
tcw.lowerNeighbors = nil
tcw.localNeighbors = nil
}

func (tcw *TriangleCountWorker) OutputValueType() string {
return common.HgValueTypeInt
}
Expand Down
157 changes: 157 additions & 0 deletions vermeer/algorithms/triangle_count_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with this
work for additional information regarding copyright ownership. The ASF
licenses this file to You under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/

package algorithms

import (
"testing"
"vermeer/apps/serialize"
)

func TestBuildTriangleNeighborStateDeduplicatesAndDropsSelfLoops(t *testing.T) {
neighbors, lower := buildTriangleNeighborState(3,
serialize.SliceUint32{1, 2, 3, 2},
serialize.SliceUint32{1, 4, 3, 4})
if len(neighbors) != 3 {
t.Fatalf("expected neighbors {1,2,4}, got %v", neighbors)
}
for _, expected := range []serialize.SUint32{1, 2, 4} {
if _, exists := neighbors[expected]; !exists {
t.Fatalf("missing neighbor %d in %v", expected, neighbors)
}
}
if len(lower) != 2 || lower[0] != 1 || lower[1] != 2 {
t.Fatalf("expected lower neighbors [1 2], got %v", lower)
}
}

func TestTriangleCountReturnsOneForEveryTriangleVertex(t *testing.T) {
// Every undirected edge is intentionally present in both directions. The
// compact state must deduplicate it before counting.
inEdges := []serialize.SliceUint32{
{1, 2},
{0, 2},
{0, 1},
}
outEdges := []serialize.SliceUint32{
{1, 2},
{0, 2},
{0, 1},
}
counts := triangleCountsForTest(inEdges, outEdges)
for vertexID, count := range counts {
if count != 1 {
t.Fatalf("vertex %d expected one incident triangle, got %d", vertexID, count)
}
}
}

func TestTriangleCountDoesNotCountOpenWedge(t *testing.T) {
inEdges := []serialize.SliceUint32{
{1},
{0, 2},
{1},
}
outEdges := []serialize.SliceUint32{
{1},
{0, 2},
{1},
}
counts := triangleCountsForTest(inEdges, outEdges)
for vertexID, count := range counts {
if count != 0 {
t.Fatalf("open wedge vertex %d reported %d triangles", vertexID, count)
}
}
}

func TestTriangleCountHandlesMixedEdgeDirections(t *testing.T) {
// The same undirected triangle is split across incoming and outgoing
// storage, matching graphs that do not materialize reciprocal edges.
inEdges := []serialize.SliceUint32{
{2},
{0},
{1},
}
outEdges := []serialize.SliceUint32{
{1},
{2},
{0},
}
counts := triangleCountsForTest(inEdges, outEdges)
for vertexID, count := range counts {
if count != 1 {
t.Fatalf("mixed-direction vertex %d expected one triangle, got %d", vertexID, count)
}
}
}

func TestCountTriangleMembershipScansCompactLowerSlice(t *testing.T) {
neighbors := map[serialize.SUint32]struct{}{
1: {},
3: {},
5: {},
7: {},
}
lower := serialize.SliceUint32{0, 1, 2, 3, 4, 5, 6}
if got := countTriangleMembership(neighbors, lower); got != 3 {
t.Fatalf("expected three shared neighbors, got %d", got)
}
}

func TestTriangleCountIgnoresInvalidRemoteNeighborList(t *testing.T) {
neighbors := []map[serialize.SUint32]struct{}{
{1: {}, 99: {}},
{0: {}},
}
lower := []serialize.SliceUint32{
{},
{0},
}
var count serialize.SUint32
for neighborID := range neighbors[0] {
if int(neighborID) >= len(lower) {
continue
}
count += countTriangleMembership(neighbors[0], lower[neighborID])
}
if count != 0 {
t.Fatalf("invalid remote ID changed triangle count: %d", count)
}
}

func triangleCountsForTest(inEdges, outEdges []serialize.SliceUint32) []serialize.SUint32 {
total := len(inEdges)
neighbors := make([]map[serialize.SUint32]struct{}, total)
lower := make([]serialize.SliceUint32, total)
for vertexID := 0; vertexID < total; vertexID++ {
neighbors[vertexID], lower[vertexID] = buildTriangleNeighborState(
uint32(vertexID), inEdges[vertexID], outEdges[vertexID])
}

counts := make([]serialize.SUint32, total)
for vertexID := 0; vertexID < total; vertexID++ {
for neighborID := range neighbors[vertexID] {
if int(neighborID) >= total {
continue
}
counts[vertexID] += countTriangleMembership(neighbors[vertexID],
lower[neighborID])
}
}
return counts
}
Loading