Skip to content

Commit

Permalink
Add simple benchmark suite (#1)
Browse files Browse the repository at this point in the history
* add a simple benchmark suite

* add a simple benchmark suite

* add a simple benchmark suite

* add a simple benchmark suite
  • Loading branch information
mtrossbach authored Aug 29, 2024
1 parent a8f42b3 commit a6785be
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# Introduction
Hypermatch is a high-performance Go library that enables rapid matching of a large number of rules against events. Designed for speed and efficiency, hypermatch handles thousands of events per second with low latency, making it ideal for real-time systems.

- **Fast Matching**: Matches events to a large set of rules in-memory with minimal delay.
- **Fast Matching**: Matches events to a large set of rules in-memory with minimal delay ... [it's really fast! (Benchmark)](_benchmark/benchmark.md)
- **Readable Rule Format**: Serialize rules into human-readable JSON objects.
- **Flexible Rule Syntax**: Supports various matching conditions, including equals, prefix, suffix, wildcard, anything-but, all-of, and any-of.

Expand Down Expand Up @@ -285,4 +285,4 @@ If the attribute value is type of:
Nevertheless, there are a few things to consider to get maximum performance:
- Shorten the number of fields inside the rules, the fewer conditions, the shorter is the path to find them out.
- Try to make the **paths** as diverse as possible in events and rules. The more heterogeneous fields, the higher the performance.
- Reduce the number of **anyOf** conditions wherever possible
- Reduce the number of **anyOf** conditions wherever possible
32 changes: 32 additions & 0 deletions _benchmark/benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Benchmarks

To run the benchmark suite, navigate to the same folder and execute the main.go file.
This suite will test the performance of hypermatch with plain Go objects, hypermatch with JSON objects, and [quamina](https://github.com/timbray/quamina) against each other.
Simply run the command go run main.go to start the benchmarks and compare the results.

Results as of Aug, 29th, 2024 with Go 1.23.0 on MacBook Pro M1 Max, 32GB RAM with 100,000 rules:

```
---Starting with hypermatch
adding 100000 rules took 0.51862s
processed 51753 events with 517530 matches in 1.00001s -> 51752.42425 evt/s
processed 103199 events with 1031990 matches in 2.00004s -> 51598.36053 evt/s
processed 156249 events with 1562490 matches in 3.00009s -> 52081.45563 evt/s
processed 209523 events with 2095230 matches in 4.00013s -> 52379.05586 evt/s
processed 262257 events with 2622570 matches in 5.00016s -> 52449.76793 evt/s
---Starting with hypermatch-json
adding 100000 rules took 1.95150s
processed 39732 events with 397320 matches in 1.00000s -> 39731.90564 evt/s
processed 80432 events with 804320 matches in 2.00003s -> 40215.31718 evt/s
processed 121915 events with 1219150 matches in 3.00006s -> 40637.55840 evt/s
processed 164093 events with 1640930 matches in 4.00009s -> 41022.33768 evt/s
processed 206235 events with 2062350 matches in 5.00013s -> 41245.96473 evt/s
---Starting with quamina
adding 100000 rules took 4.54697s
processed 4 events with 0 matches in 1.24154s -> 3.22181 evt/s
processed 7 events with 0 matches in 2.31263s -> 3.02685 evt/s
processed 11 events with 0 matches in 3.50818s -> 3.13552 evt/s
processed 15 events with 0 matches in 4.70148s -> 3.19049 evt/s
```
78 changes: 78 additions & 0 deletions _benchmark/candidates/hypermatch_lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package candidates

import (
"fmt"
"github.com/SchwarzIT/hypermatch"
"log"
)

type Hypermatch struct {
h *hypermatch.HyperMatch
}

func NewHypermatch() *Hypermatch {
return &Hypermatch{h: hypermatch.NewHyperMatch()}
}

func (h *Hypermatch) Name() string {
return "hypermatch"
}

func (h *Hypermatch) AddRule(number int, modulo int) {
err := h.h.AddRule(hypermatch.RuleIdentifier(number), hypermatch.ConditionSet{
{Path: "name", Pattern: hypermatch.Pattern{Type: hypermatch.PatternWildcard, Value: "*-myapp-*"}},
{Path: "env", Pattern: hypermatch.Pattern{Type: hypermatch.PatternEquals, Value: "prod"}},
{Path: "number", Pattern: hypermatch.Pattern{Type: hypermatch.PatternEquals, Value: fmt.Sprintf("%d", number%modulo)}},
{Path: "tags", Pattern: hypermatch.Pattern{
Type: hypermatch.PatternAllOf, Sub: []hypermatch.Pattern{
{Type: hypermatch.PatternEquals, Value: "tag1"},
{Type: hypermatch.PatternEquals, Value: "tag2"},
},
}},
{Path: "region", Pattern: hypermatch.Pattern{
Type: hypermatch.PatternAnythingBut, Sub: []hypermatch.Pattern{
{Type: hypermatch.PatternEquals, Value: "moon"},
},
}},
{Path: "type", Pattern: hypermatch.Pattern{
Type: hypermatch.PatternAnyOf, Sub: []hypermatch.Pattern{
{Type: hypermatch.PatternEquals, Value: "app"},
{Type: hypermatch.PatternEquals, Value: "database"},
},
}},
})
if err != nil {
log.Panicln(err)
}
}

func (h *Hypermatch) Match(number int, modulo int) int {
event := []hypermatch.Property{
{
Path: "name",
Values: []string{fmt.Sprintf("app-myapp-%d", number)},
},
{
Path: "env",
Values: []string{"prod"},
},
{
Path: "number",
Values: []string{fmt.Sprintf("%d", number%modulo)},
},
{
Path: "tags",
Values: []string{"tag1", "tag2"},
},
{
Path: "region",
Values: []string{"earth"},
},
{
Path: "type",
Values: []string{"app"},
},
}
matches := h.h.Match(event)
return len(matches)
}
60 changes: 60 additions & 0 deletions _benchmark/candidates/hypermatchjson_lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package candidates

import (
"encoding/json"
"fmt"
"github.com/SchwarzIT/hypermatch"
)

type HypermatchJson struct {
h *hypermatch.HyperMatch
}

func NewHypermatchJson() *HypermatchJson {
return &HypermatchJson{h: hypermatch.NewHyperMatch()}
}

func (h *HypermatchJson) Name() string {
return "hypermatch-json"
}

func (h *HypermatchJson) AddRule(number int, modulo int) {
jsonStr := fmt.Sprintf(`
{
"name": {"wildcard": "*-myapp-*"},
"env": {"equals": "prod"},
"number": {"equals": "%d"},
"tags": {"allOf": [{"equals": "tag1"}, {"equals": "tag2"}]},
"region": {"anythingBut": [{"equals": "moon"}]},
"type": {"anyOf": [{"equals": "app"}, {"equals": "database"}]}
}
`, number%modulo)
var conditionSet hypermatch.ConditionSet
if err := json.Unmarshal([]byte(jsonStr), &conditionSet); err != nil {
panic(err)
}
if err := h.h.AddRule(hypermatch.RuleIdentifier(number), conditionSet); err != nil {
panic(err)
}
}

func (h *HypermatchJson) Match(number int, modulo int) int {
eventStr := fmt.Sprintf(`
[
{"Path": "name", "Values": ["app-myapp-%d"]},
{"Path": "env", "Values": ["prod"]},
{"Path": "number", "Values": ["%d"]},
{"Path": "tags", "Values": ["tag1", "tag2"]},
{"Path": "region", "Values": ["earth"]},
{"Path": "type", "Values": ["app"]}
]
`, number, number%modulo)

var properties []hypermatch.Property
if err := json.Unmarshal([]byte(eventStr), &properties); err != nil {
panic(err)
}

matches := h.h.Match(properties)
return len(matches)
}
55 changes: 55 additions & 0 deletions _benchmark/candidates/quamina.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package candidates

import (
"fmt"
"log"
"quamina.net/go/quamina"
)

type Quamina struct {
q *quamina.Quamina
}

func NewQuamina() *Quamina {
q, err := quamina.New(quamina.WithMediaType("application/json"))
if err != nil {
panic(err)
}
return &Quamina{q: q}
}

func (q *Quamina) Name() string {
return "quamina"
}

func (q *Quamina) AddRule(number int, modulo int) {
str := fmt.Sprintf(`
{
"name": [{"shellstyle": "*-myapp-*"}],
"env": ["prod"],
"nunmber": ["%d"],
"tags": ["tag1", "tag2"],
"region": [{"anything-but": ["moon"]}],
"type": ["app", "database"]
}
`, number%modulo)
err := q.q.AddPattern(number, str)
if err != nil {
log.Panicln(err)
}
}

func (q *Quamina) Match(number int, modulo int) int {
event := fmt.Sprintf(`
{
"name": "app-myapp-%d",
"env": "prod",
"number": "%d",
"tags": ["tag1", "tag2"],
"region": "earth",
"type": "app"
}
`, number, number%modulo)
r, _ := q.q.MatchesForEvent([]byte(event))
return len(r)
}
8 changes: 8 additions & 0 deletions _benchmark/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module benchmark

go 1.23.0

require (
github.com/SchwarzIT/hypermatch v0.1.0
quamina.net/go/quamina v1.3.0
)
8 changes: 8 additions & 0 deletions _benchmark/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/SchwarzIT/hypermatch v0.1.0 h1:ytJivIAFP++88WaiIFD/7+yl6Yz4gPfVNrodMe6Tmqc=
github.com/SchwarzIT/hypermatch v0.1.0/go.mod h1:H/WStKuHk4FprRLaR6nBC2PY1oKNqIsDysiKREZLLcY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
quamina.net/go/quamina v1.3.0 h1:8CI8InbNYbswmnda70fU2YItHxEb4cmq0p0mttBKL2w=
quamina.net/go/quamina v1.3.0/go.mod h1:EJ1teLWOcAHYfOUE+w2B6OQq5sAxEiwE0EDlcRxx+TQ=
57 changes: 57 additions & 0 deletions _benchmark/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"benchmark/candidates"
"context"
"log"
"time"
)

const (
numberOfRules = 100000
eventCheckDuration = 5 * time.Second
)

type Candidate interface {
Name() string
AddRule(number int, modulo int)
Match(number int, modulo int) int
}

func main() {
cs := []Candidate{candidates.NewHypermatch(), candidates.NewHypermatchJson(), candidates.NewQuamina()}

for _, c := range cs {
log.Printf("---Starting with %s\n", c.Name())
beforeAddingRules := time.Now()

for i := 0; i < numberOfRules; i++ {
c.AddRule(i, numberOfRules/10)
}
log.Printf("adding %d rules took %.5fs\n", numberOfRules, time.Since(beforeAddingRules).Seconds())
runEvents(c)
}
}

func runEvents(c Candidate) {
numberOfEvents := 0
numberOfMatches := 0
beforeCheckingEvents := time.Now()
lastPrint := beforeCheckingEvents
ctx, cancel := context.WithTimeout(context.Background(), eventCheckDuration)
defer cancel()
for {
select {
case <-ctx.Done():
return
default:
numberOfMatches += c.Match(numberOfMatches, numberOfRules/10)
numberOfEvents += 1

if time.Since(lastPrint).Seconds() >= 1 {
log.Printf("processed %d events with %d matches in %.5fs -> %.5f evt/s\n", numberOfEvents, numberOfMatches, time.Since(beforeCheckingEvents).Seconds(), float64(numberOfEvents)/time.Since(beforeCheckingEvents).Seconds())
lastPrint = time.Now()
}
}
}
}

0 comments on commit a6785be

Please sign in to comment.