diff --git a/envelopes_benchmark_test.go b/envelopes_benchmark_test.go index 68fa2bf..4f7da82 100644 --- a/envelopes_benchmark_test.go +++ b/envelopes_benchmark_test.go @@ -1,114 +1,257 @@ package nostr import ( - "bufio" - "os" + "fmt" + "math/rand/v2" "testing" + "time" "github.com/minio/simdjson-go" ) -// benchmarkParseMessage benchmarks the ParseMessage function func BenchmarkParseMessage(b *testing.B) { - messages := getTestMessages() - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _ = ParseMessage(msg) + messages := generateTestMessages(2000) + + for i := range 15 { + fmt.Println(string(messages[i])) + } + + b.Run("golang", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, msg := range messages { + _ = ParseMessage(msg) + } + } + }) + + b.Run("simdjson", func(b *testing.B) { + pj := &simdjson.ParsedJson{} + for i := 0; i < b.N; i++ { + for _, msg := range messages { + _, _ = ParseMessageSIMD(msg, pj) + } } + }) +} + +func generateTestMessages(count int) [][]byte { + messages := make([][]byte, 0, count) + + for i := 0; i < count; i++ { + var msg []byte + + switch rand.IntN(12) { + case 1: + msg = generateAuthMessage() + case 2: + msg = generateNoticeMessage() + case 3: + msg = generateEOSEMessage() + case 4: + msg = generateOKMessage() + case 5: + msg = generateCountMessage() + case 6: + msg = generateReqMessage() + default: + msg = generateEventMessage() + } + + messages = append(messages, msg) } + + return messages } -// benchmarkParseMessageSIMD benchmarks the ParseMessageSIMD function -func BenchmarkParseMessageSIMD(b *testing.B) { - messages := getTestMessages() - var pj *simdjson.ParsedJson - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _, _ = ParseMessageSIMD(msg, pj) +func generateEventMessage() []byte { + event := generateRandomEvent() + eventJSON, _ := json.Marshal(event) + + if rand.IntN(2) == 0 { + subID := fmt.Sprintf("sub_%d", rand.IntN(1000)) + return []byte(fmt.Sprintf(`["EVENT","%s",%s]`, subID, string(eventJSON))) + } + + return []byte(fmt.Sprintf(`["EVENT",%s]`, string(eventJSON))) +} + +func generateRandomEvent() Event { + tagCount := rand.IntN(10) + tags := make(Tags, tagCount) + for i := 0; i < tagCount; i++ { + tagType := string([]byte{byte('a' + rand.IntN(26))}) + tagValues := make([]string, rand.IntN(5)+1) + for j := range tagValues { + tagValues[j] = fmt.Sprintf("value_%d_%d", i, j) } + tags[i] = append([]string{tagType}, tagValues...) } + + contentLength := rand.IntN(200) + 10 + content := make([]byte, contentLength) + for i := range content { + content[i] = byte('a' + rand.IntN(26)) + } + + event := Event{ + ID: generateRandomHex(64), + PubKey: generateRandomHex(64), + CreatedAt: Timestamp(time.Now().Unix() - int64(rand.IntN(10000000))), + Kind: rand.IntN(10000), + Tags: tags, + Content: string(content), + Sig: generateRandomHex(128), + } + + return event } -// benchmarkParseMessageSIMDWithReuse benchmarks the ParseMessageSIMD function with reusing the ParsedJson object -func BenchmarkParseMessageSIMDWithReuse(b *testing.B) { - messages := getTestMessages() - pj, _ := simdjson.Parse(make([]byte, 1024), nil) // initialize with some capacity - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _, _ = ParseMessageSIMD(msg, pj) +func generateAuthMessage() []byte { + if rand.IntN(2) == 0 { + challenge := fmt.Sprintf("challenge_%d", rand.IntN(1000000)) + return []byte(fmt.Sprintf(`["AUTH","%s"]`, challenge)) + } else { + event := generateRandomEvent() + eventJSON, _ := json.Marshal(event) + return []byte(fmt.Sprintf(`["AUTH",%s]`, string(eventJSON))) + } +} + +func generateNoticeMessage() []byte { + noticeLength := rand.IntN(100) + 5 + notice := make([]byte, noticeLength) + for i := range notice { + notice[i] = byte('a' + rand.IntN(26)) + } + + return []byte(fmt.Sprintf(`["NOTICE","%s"]`, string(notice))) +} + +func generateEOSEMessage() []byte { + subID := fmt.Sprintf("sub_%d", rand.IntN(1000)) + return []byte(fmt.Sprintf(`["EOSE","%s"]`, subID)) +} + +func generateOKMessage() []byte { + eventID := generateRandomHex(64) + success := rand.IntN(2) == 0 + + var reason string + if !success { + reasons := []string{ + "blocked", + "rate-limited", + "invalid: signature verification failed", + "error: could not connect to the database", + "pow: difficulty too low", } + reason = reasons[rand.IntN(len(reasons))] } + + return []byte(fmt.Sprintf(`["OK","%s",%t,"%s"]`, eventID, success, reason)) } -// getTestMessages returns a slice of test messages for benchmarking -func getTestMessages() [][]byte { - // these are sample messages from the test cases - return [][]byte{ - []byte(`["EVENT","_",{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}]`), - []byte(`["EVENT",{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}]`), - []byte(`["AUTH","challenge-string"]`), - []byte(`["AUTH",{"kind":22242,"id":"9b86ca5d2a9b4aa09870e710438c2fd2fcdeca12a18b6f17ab9ebcdbc43f1d4a","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1740505646,"tags":[["relay","ws://localhost:7777","2"],["challenge","3027526784722639360"]],"content":"","sig":"eceb827c4bba1de0ab8ee43f3e98df71194f5bdde0af27b5cda38e5c4338b5f63d31961acb5e3c119fd00ecef8b469867d060b697dbaa6ecee1906b483bc307d"}]`), - []byte(`["NOTICE","test notice message"]`), - []byte(`["EOSE","subscription123"]`), - []byte(`["CLOSE","subscription123"]`), - []byte(`["CLOSED","subscription123","reason: test closed"]`), - []byte(`["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",true,""]`), - []byte(`["COUNT","sub1",{"count":42}]`), - []byte(`["REQ","sub1",{"until":999999,"kinds":[1]}]`), - []byte(`["REQ","sub1z\\\"zzz",{"authors":["9b86ca5d2a9b4aa09870e710438c2fd2fcdeca12a18b6f17ab9ebcdbc43f1d4a","8eee10b2ce1162b040fdcfdadb4f888c64aaf87531dab28cc0c09fbdea1b663e","0deadebefb3c1a760f036952abf675076343dd8424efdeaa0f1d9803a359ed46"],"since":1740425099,"limit":2,"#x":["<","as"]},{"kinds":[2345,112],"#plic":["a"],"#ploc":["blblb","wuwuw"]}]`), +func generateCountMessage() []byte { + subID := fmt.Sprintf("sub_%d", rand.IntN(1000)) + count := rand.IntN(10000) + + if rand.IntN(5) == 0 { + hll := generateRandomHex(512) + return []byte(fmt.Sprintf(`["COUNT","%s",{"count":%d,"hll":"%s"}]`, subID, count, hll)) } + + return []byte(fmt.Sprintf(`["COUNT","%s",{"count":%d}]`, subID, count)) } -// benchmarkParseMessagesFromFile benchmarks parsing messages from a file -// this function can be used if you have a file with messages -func BenchmarkParseMessagesFromFile(b *testing.B) { - // read all messages into memory - file, err := os.Open("testdata/messages.json") - if err != nil { - b.Fatal(err) +func generateReqMessage() []byte { + subID := fmt.Sprintf("sub_%d", rand.IntN(1000)) + + filterCount := rand.IntN(3) + 1 + filters := make([]string, filterCount) + + for i := range filters { + filter := generateRandomFilter() + filterJSON, _ := json.Marshal(filter) + filters[i] = string(filterJSON) } - var messages [][]byte - scanner := bufio.NewScanner(file) - for scanner.Scan() { - messages = append(messages, []byte(scanner.Text())) + + result := fmt.Sprintf(`["REQ","%s"`, subID) + for _, f := range filters { + result += "," + f } - if err := scanner.Err(); err != nil { - b.Fatal(err) + result += "]" + + return []byte(result) +} + +func generateRandomFilter() Filter { + filter := Filter{} + + if rand.IntN(2) == 0 { + count := rand.IntN(5) + 1 + filter.IDs = make([]string, count) + for i := range filter.IDs { + filter.IDs[i] = generateRandomHex(64) + } } - file.Close() - // benchmark ParseMessage - b.Run("ParseMessage", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _ = ParseMessage(msg) - } + if rand.IntN(2) == 0 { + count := rand.IntN(5) + 1 + filter.Kinds = make([]int, count) + for i := range filter.Kinds { + filter.Kinds[i] = rand.IntN(10000) } - }) + } - // benchmark ParseMessageSIMD - b.Run("ParseMessageSIMD", func(b *testing.B) { - var pj *simdjson.ParsedJson - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _, _ = ParseMessageSIMD(msg, pj) - } + if rand.IntN(2) == 0 { + count := rand.IntN(5) + 1 + filter.Authors = make([]string, count) + for i := range filter.Authors { + filter.Authors[i] = generateRandomHex(64) } - }) + } - // benchmark ParseMessageSIMD with reuse - b.Run("ParseMessageSIMDWithReuse", func(b *testing.B) { - pj, _ := simdjson.Parse(make([]byte, 1024), nil) // initialize with some capacity - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _, _ = ParseMessageSIMD(msg, pj) + if rand.IntN(2) == 0 { + tagCount := rand.IntN(3) + 1 + filter.Tags = make(TagMap) + + for i := 0; i < tagCount; i++ { + tagName := string([]byte{byte('a' + rand.IntN(26))}) + valueCount := rand.IntN(3) + 1 + values := make([]string, valueCount) + + for j := range values { + values[j] = fmt.Sprintf("tag_value_%d", rand.IntN(100)) } + + filter.Tags[tagName] = values } - }) + } + + if rand.IntN(2) == 0 { + ts := Timestamp(time.Now().Unix() - int64(rand.IntN(10000000))) + filter.Since = &ts + } + + if rand.IntN(2) == 0 { + ts := Timestamp(time.Now().Unix() - int64(rand.IntN(1000000))) + filter.Until = &ts + } + + if rand.IntN(2) == 0 { + filter.Limit = rand.IntN(100) + 1 + } + + return filter +} + +func generateRandomHex(length int) string { + const hexChars = "0123456789abcdef" + result := make([]byte, length) + + for i := range result { + result[i] = hexChars[rand.IntN(len(hexChars))] + } + + return string(result) } diff --git a/envelopes_test.go b/envelopes_test.go index 10732b1..ca64dcd 100644 --- a/envelopes_test.go +++ b/envelopes_test.go @@ -321,8 +321,7 @@ func TestParseMessageSIMD(t *testing.T) { } require.NotNil(t, envelope, "expected non-nil envelope but got nil") - require.Equal(t, testCase.ExpectedEnvelope.Label(), envelope.Label()) - require.Equal(t, testCase.ExpectedEnvelope.String(), envelope.String()) + require.Equal(t, testCase.ExpectedEnvelope, envelope) }) } }