Skip to content

Commit 5b4f860

Browse files
committed
add test for sse and ls tx timings
1 parent c2d4b72 commit 5b4f860

File tree

3 files changed

+375
-0
lines changed

3 files changed

+375
-0
lines changed

examples/tx-timing/go.mod

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module github.com/tonkeeper.tonapi-go/examples/tx-timing
2+
3+
go 1.22.0
4+
5+
require (
6+
github.com/r3labs/sse/v2 v2.10.0
7+
github.com/tonkeeper/tongo v1.9.0
8+
)
9+
10+
require (
11+
github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect
12+
github.com/snksoft/crc v1.1.0 // indirect
13+
github.com/stretchr/testify v1.10.0 // indirect
14+
golang.org/x/crypto v0.29.0 // indirect
15+
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
16+
golang.org/x/net v0.31.0 // indirect
17+
golang.org/x/sys v0.27.0 // indirect
18+
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
19+
)

examples/tx-timing/go.sum

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY=
5+
github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=
6+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8+
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
9+
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
10+
github.com/snksoft/crc v1.1.0 h1:HkLdI4taFlgGGG1KvsWMpz78PkOC9TkPVpTV/cuWn48=
11+
github.com/snksoft/crc v1.1.0/go.mod h1:5/gUOsgAm7OmIhb6WJzw7w5g2zfJi4FrHYgGPdshE+A=
12+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
14+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
15+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
16+
github.com/tonkeeper/tongo v1.9.0 h1:yWPc13byc341mnKOBbPkBzGs9GxGdZ+ugMIBg+Q2pNk=
17+
github.com/tonkeeper/tongo v1.9.0/go.mod h1:MjgIgAytFarjCoVjMLjYEtpZNN1f2G/pnZhKjr28cWs=
18+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19+
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
20+
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
21+
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
22+
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
23+
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
24+
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
25+
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
26+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
27+
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
28+
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
29+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
30+
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
31+
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
32+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
33+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
34+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
35+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

examples/tx-timing/main.go

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"sort"
8+
"sync"
9+
"time"
10+
11+
sse "github.com/r3labs/sse/v2"
12+
"github.com/tonkeeper/tongo/liteapi"
13+
"github.com/tonkeeper/tongo/tlb"
14+
"github.com/tonkeeper/tongo/ton"
15+
"github.com/tonkeeper/tongo/wallet"
16+
)
17+
18+
const (
19+
numRuns = 20
20+
)
21+
22+
type RunResult struct {
23+
SSELatency time.Duration
24+
LiteLatency time.Duration
25+
SSESuccess bool
26+
LiteSuccess bool
27+
}
28+
29+
func main() {
30+
seed := ""
31+
destinationStr := ""
32+
apiKey := ""
33+
amount := uint64(10_000_000) // 0.01 TON
34+
35+
destination := ton.MustParseAccountID(destinationStr)
36+
37+
cli, err := liteapi.NewClient(liteapi.Testnet())
38+
if err != nil {
39+
fmt.Printf("failed to create liteapi client: %v\n", err)
40+
os.Exit(1)
41+
}
42+
43+
privateKey, err := wallet.SeedToPrivateKey(seed)
44+
if err != nil {
45+
fmt.Printf("failed to convert seed to private key: %v\n", err)
46+
os.Exit(1)
47+
}
48+
49+
w, err := wallet.New(privateKey, wallet.V3R2, cli)
50+
if err != nil {
51+
fmt.Printf("failed to create wallet: %v\n", err)
52+
os.Exit(1)
53+
}
54+
55+
walletAddress := w.GetAddress()
56+
fmt.Printf("Wallet address: %v\n", walletAddress.ToHuman(true, false))
57+
fmt.Printf("Running %d tests...\n\n", numRuns)
58+
59+
var results []RunResult
60+
var sseLatencies, liteLatencies []time.Duration
61+
62+
for i := 1; i <= numRuns; i++ {
63+
fmt.Printf("=== RUN %d/%d ===\n", i, numRuns)
64+
65+
seqnoBefore, err := cli.GetSeqno(context.Background(), walletAddress)
66+
if err != nil {
67+
fmt.Printf(" failed to get seqno: %v\n", err)
68+
continue
69+
}
70+
71+
result := runTest(cli, w, walletAddress, destination, amount, apiKey)
72+
results = append(results, result)
73+
74+
if result.SSESuccess {
75+
sseLatencies = append(sseLatencies, result.SSELatency)
76+
fmt.Printf(" SSE: %v\n", result.SSELatency)
77+
} else {
78+
fmt.Printf(" SSE: ERROR\n")
79+
}
80+
81+
if result.LiteSuccess {
82+
liteLatencies = append(liteLatencies, result.LiteLatency)
83+
fmt.Printf(" Liteserver: %v\n", result.LiteLatency)
84+
} else {
85+
fmt.Printf(" Liteserver: ERROR\n")
86+
}
87+
88+
fmt.Println()
89+
90+
if i < numRuns {
91+
fmt.Printf(" Waiting for seqno to increment...")
92+
waitForSeqnoIncrement(cli, walletAddress, seqnoBefore)
93+
fmt.Printf(" done\n")
94+
}
95+
}
96+
97+
fmt.Printf("\n========== FINAL RESULTS ==========\n\n")
98+
99+
if len(sseLatencies) > 0 {
100+
fmt.Printf("SSE Statistics (%d successful runs):\n", len(sseLatencies))
101+
fmt.Printf(" Average: %v\n", average(sseLatencies))
102+
fmt.Printf(" Min: %v\n", min(sseLatencies))
103+
fmt.Printf(" Max: %v\n", max(sseLatencies))
104+
fmt.Printf(" Median: %v\n", median(sseLatencies))
105+
} else {
106+
fmt.Printf("SSE: No successful runs\n")
107+
}
108+
109+
fmt.Println()
110+
111+
if len(liteLatencies) > 0 {
112+
fmt.Printf("Liteserver Statistics (%d successful runs):\n", len(liteLatencies))
113+
fmt.Printf(" Average: %v\n", average(liteLatencies))
114+
fmt.Printf(" Min: %v\n", min(liteLatencies))
115+
fmt.Printf(" Max: %v\n", max(liteLatencies))
116+
fmt.Printf(" Median: %v\n", median(liteLatencies))
117+
} else {
118+
fmt.Printf("Liteserver: No successful runs\n")
119+
}
120+
}
121+
122+
func runTest(cli *liteapi.Client, w wallet.Wallet, walletAddress ton.AccountID, destination ton.AccountID, amount uint64, apiKey string) RunResult {
123+
result := RunResult{}
124+
125+
initialState, err := cli.GetAccountState(context.Background(), walletAddress)
126+
if err != nil {
127+
fmt.Printf("failed to get initial account state: %v\n", err)
128+
return result
129+
}
130+
initialLT := initialState.Account.Account.Storage.LastTransLt
131+
132+
sseFoundCh := make(chan time.Time, 1)
133+
liteFoundCh := make(chan time.Time, 1)
134+
ctx, cancel := context.WithCancel(context.Background())
135+
defer cancel()
136+
137+
var wg sync.WaitGroup
138+
139+
wg.Add(1)
140+
go func() {
141+
defer wg.Done()
142+
listenSSE(ctx, walletAddress.ToRaw(), apiKey, sseFoundCh)
143+
}()
144+
145+
time.Sleep(500 * time.Millisecond)
146+
147+
sendUtime := time.Now().UTC().Unix()
148+
msg := wallet.SimpleTransfer{
149+
Amount: tlb.Grams(amount),
150+
Address: destination,
151+
Comment: fmt.Sprintf("timing test %d", sendUtime),
152+
}
153+
154+
sendBeforeTime := time.Now()
155+
err = w.Send(context.Background(), msg)
156+
if err != nil {
157+
fmt.Printf("failed to send transaction: %v\n", err)
158+
cancel()
159+
return result
160+
}
161+
162+
sendTime := time.Now()
163+
fmt.Printf(" Send time: %v\n", sendTime.Sub(sendBeforeTime))
164+
165+
wg.Add(1)
166+
go func() {
167+
defer wg.Done()
168+
pollLiteserver(ctx, cli, walletAddress, initialLT, liteFoundCh)
169+
}()
170+
171+
timeout := time.After(60 * time.Second)
172+
173+
for !result.SSESuccess || !result.LiteSuccess {
174+
select {
175+
case t := <-sseFoundCh:
176+
if !result.SSESuccess {
177+
result.SSELatency = t.Sub(sendTime)
178+
result.SSESuccess = true
179+
}
180+
case t := <-liteFoundCh:
181+
if !result.LiteSuccess {
182+
result.LiteLatency = t.Sub(sendTime)
183+
result.LiteSuccess = true
184+
}
185+
case <-timeout:
186+
cancel()
187+
return result
188+
}
189+
}
190+
191+
cancel()
192+
return result
193+
}
194+
195+
func listenSSE(ctx context.Context, accountRaw string, apiKey string, foundCh chan<- time.Time) {
196+
url := fmt.Sprintf("https://rt-testnet.tonapi.io/sse/transactions?account=%s", accountRaw)
197+
198+
client := sse.NewClient(url)
199+
client.Headers = map[string]string{
200+
"Authorization": fmt.Sprintf("Bearer %s", apiKey),
201+
}
202+
203+
err := client.SubscribeWithContext(ctx, "", func(msg *sse.Event) {
204+
switch string(msg.Event) {
205+
case "heartbeat":
206+
return
207+
case "message":
208+
select {
209+
case foundCh <- time.Now():
210+
default:
211+
}
212+
}
213+
})
214+
215+
if err != nil && ctx.Err() == nil {
216+
fmt.Printf("SSE error: %v\n", err)
217+
}
218+
}
219+
220+
func waitForSeqnoIncrement(cli *liteapi.Client, account ton.AccountID, seqnoBefore uint32) {
221+
ticker := time.NewTicker(500 * time.Millisecond)
222+
defer ticker.Stop()
223+
224+
timeout := time.After(30 * time.Second)
225+
226+
for {
227+
select {
228+
case <-timeout:
229+
return
230+
case <-ticker.C:
231+
seqno, err := cli.GetSeqno(context.Background(), account)
232+
if err != nil {
233+
continue
234+
}
235+
if seqno > seqnoBefore {
236+
return
237+
}
238+
}
239+
}
240+
}
241+
242+
func pollLiteserver(ctx context.Context, cli *liteapi.Client, account ton.AccountID, initialLT uint64, foundCh chan<- time.Time) {
243+
ticker := time.NewTicker(50 * time.Millisecond)
244+
defer ticker.Stop()
245+
246+
for {
247+
select {
248+
case <-ctx.Done():
249+
return
250+
case <-ticker.C:
251+
state, err := cli.GetAccountState(ctx, account)
252+
if err != nil {
253+
if ctx.Err() != nil {
254+
return
255+
}
256+
continue
257+
}
258+
259+
currentLT := state.Account.Account.Storage.LastTransLt
260+
if currentLT > initialLT {
261+
select {
262+
case foundCh <- time.Now():
263+
default:
264+
}
265+
return
266+
}
267+
}
268+
}
269+
}
270+
271+
func average(durations []time.Duration) time.Duration {
272+
if len(durations) == 0 {
273+
return 0
274+
}
275+
var sum time.Duration
276+
for _, d := range durations {
277+
sum += d
278+
}
279+
return sum / time.Duration(len(durations))
280+
}
281+
282+
func min(durations []time.Duration) time.Duration {
283+
if len(durations) == 0 {
284+
return 0
285+
}
286+
m := durations[0]
287+
for _, d := range durations[1:] {
288+
if d < m {
289+
m = d
290+
}
291+
}
292+
return m
293+
}
294+
295+
func max(durations []time.Duration) time.Duration {
296+
if len(durations) == 0 {
297+
return 0
298+
}
299+
m := durations[0]
300+
for _, d := range durations[1:] {
301+
if d > m {
302+
m = d
303+
}
304+
}
305+
return m
306+
}
307+
308+
func median(durations []time.Duration) time.Duration {
309+
if len(durations) == 0 {
310+
return 0
311+
}
312+
sorted := make([]time.Duration, len(durations))
313+
copy(sorted, durations)
314+
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
315+
316+
n := len(sorted)
317+
if n%2 == 0 {
318+
return (sorted[n/2-1] + sorted[n/2]) / 2
319+
}
320+
return sorted[n/2]
321+
}

0 commit comments

Comments
 (0)