230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
package loadharness
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/iamxvbaba/td/tg"
|
|
)
|
|
|
|
const deliveryMarkerPrefix = "telesrv-load-v3"
|
|
|
|
type deliverySource uint8
|
|
|
|
const (
|
|
deliveryLive deliverySource = 1 << iota
|
|
deliveryDifference
|
|
)
|
|
|
|
type deliveryExpectation struct {
|
|
senderUserID int64
|
|
targetUserID int64
|
|
startedAt time.Time
|
|
committed bool
|
|
}
|
|
|
|
type deliveryObservation struct {
|
|
sources deliverySource
|
|
repeats uint64
|
|
firstAt time.Time
|
|
}
|
|
|
|
type deliveryTracker struct {
|
|
mu sync.Mutex
|
|
runID string
|
|
expected map[string]deliveryExpectation
|
|
observations map[string]map[int64]deliveryObservation
|
|
}
|
|
|
|
type DeliveryReport struct {
|
|
RunID string `json:"run_id"`
|
|
Expected uint64 `json:"expected"`
|
|
Delivered uint64 `json:"delivered"`
|
|
Missing uint64 `json:"missing"`
|
|
LiveDelivered uint64 `json:"live_delivered"`
|
|
DifferenceRecovered uint64 `json:"difference_recovered"`
|
|
DuplicateObservations uint64 `json:"duplicate_observations"`
|
|
WrongAccountObserved uint64 `json:"wrong_account_observed"`
|
|
UnmatchedMarkers uint64 `json:"unmatched_markers"`
|
|
E2EP50MS float64 `json:"e2e_p50_ms"`
|
|
E2EP95MS float64 `json:"e2e_p95_ms"`
|
|
E2EP99MS float64 `json:"e2e_p99_ms"`
|
|
E2EMaxMS float64 `json:"e2e_max_ms"`
|
|
}
|
|
|
|
func newDeliveryTracker(runID string) *deliveryTracker {
|
|
return &deliveryTracker{
|
|
runID: runID,
|
|
expected: make(map[string]deliveryExpectation),
|
|
observations: make(map[string]map[int64]deliveryObservation),
|
|
}
|
|
}
|
|
|
|
func (t *deliveryTracker) marker(senderIndex int, sequence uint64) string {
|
|
return fmt.Sprintf("%s/%s/%d/%d", deliveryMarkerPrefix, t.runID, senderIndex, sequence)
|
|
}
|
|
|
|
func (t *deliveryTracker) expect(marker string, senderUserID, targetUserID int64) {
|
|
t.begin(marker, senderUserID, targetUserID, time.Now())
|
|
t.finish(marker, true)
|
|
}
|
|
|
|
func (t *deliveryTracker) begin(marker string, senderUserID, targetUserID int64, startedAt time.Time) {
|
|
if t == nil || !t.matches(marker) {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
t.expected[marker] = deliveryExpectation{senderUserID: senderUserID, targetUserID: targetUserID, startedAt: startedAt}
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *deliveryTracker) finish(marker string, success bool) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
expectation, ok := t.expected[marker]
|
|
if ok && success {
|
|
expectation.committed = true
|
|
t.expected[marker] = expectation
|
|
} else if ok {
|
|
delete(t.expected, marker)
|
|
}
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *deliveryTracker) observe(marker string, accountUserID int64, source deliverySource) {
|
|
if t == nil || accountUserID <= 0 || !t.matches(marker) {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
byAccount := t.observations[marker]
|
|
if byAccount == nil {
|
|
byAccount = make(map[int64]deliveryObservation)
|
|
t.observations[marker] = byAccount
|
|
}
|
|
observation := byAccount[accountUserID]
|
|
if observation.firstAt.IsZero() {
|
|
observation.firstAt = time.Now()
|
|
}
|
|
if observation.sources&source != 0 {
|
|
observation.repeats++
|
|
}
|
|
observation.sources |= source
|
|
byAccount[accountUserID] = observation
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *deliveryTracker) matches(marker string) bool {
|
|
parts := strings.Split(marker, "/")
|
|
if len(parts) != 4 || parts[0] != deliveryMarkerPrefix || parts[1] != t.runID {
|
|
return false
|
|
}
|
|
if _, err := strconv.Atoi(parts[2]); err != nil {
|
|
return false
|
|
}
|
|
sequence, err := strconv.ParseUint(parts[3], 10, 64)
|
|
return err == nil && sequence > 0
|
|
}
|
|
|
|
func (t *deliveryTracker) report() DeliveryReport {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
report := DeliveryReport{RunID: t.runID}
|
|
latencies := make([]time.Duration, 0, len(t.expected))
|
|
for marker, expectation := range t.expected {
|
|
if !expectation.committed {
|
|
continue
|
|
}
|
|
report.Expected++
|
|
byAccount := t.observations[marker]
|
|
observation, delivered := byAccount[expectation.targetUserID]
|
|
if delivered {
|
|
report.Delivered++
|
|
switch {
|
|
case observation.sources&deliveryLive != 0:
|
|
report.LiveDelivered++
|
|
case observation.sources&deliveryDifference != 0:
|
|
report.DifferenceRecovered++
|
|
}
|
|
report.DuplicateObservations += observation.repeats
|
|
if !expectation.startedAt.IsZero() && !observation.firstAt.Before(expectation.startedAt) {
|
|
latencies = append(latencies, observation.firstAt.Sub(expectation.startedAt))
|
|
}
|
|
} else {
|
|
report.Missing++
|
|
}
|
|
for accountUserID, other := range byAccount {
|
|
if accountUserID == expectation.targetUserID || accountUserID == expectation.senderUserID {
|
|
continue
|
|
}
|
|
report.WrongAccountObserved++
|
|
report.DuplicateObservations += other.repeats
|
|
}
|
|
}
|
|
for marker := range t.observations {
|
|
if expectation, ok := t.expected[marker]; !ok || !expectation.committed {
|
|
report.UnmatchedMarkers++
|
|
}
|
|
}
|
|
if len(latencies) > 0 {
|
|
report.E2EP50MS = deliveryQuantileMS(latencies, 0.50)
|
|
report.E2EP95MS = deliveryQuantileMS(latencies, 0.95)
|
|
report.E2EP99MS = deliveryQuantileMS(latencies, 0.99)
|
|
report.E2EMaxMS = deliveryQuantileMS(latencies, 1)
|
|
}
|
|
return report
|
|
}
|
|
|
|
func deliveryQuantileMS(values []time.Duration, quantile float64) float64 {
|
|
sorted := append([]time.Duration(nil), values...)
|
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
|
|
index := int(math.Ceil(float64(len(sorted))*quantile)) - 1
|
|
index = max(0, min(index, len(sorted)-1))
|
|
return durationMS(sorted[index])
|
|
}
|
|
|
|
func observeUpdatesClass(tracker *deliveryTracker, accountUserID int64, updates tg.UpdatesClass, source deliverySource) {
|
|
switch value := updates.(type) {
|
|
case *tg.Updates:
|
|
observeUpdateClasses(tracker, accountUserID, value.Updates, source)
|
|
case *tg.UpdatesCombined:
|
|
observeUpdateClasses(tracker, accountUserID, value.Updates, source)
|
|
case *tg.UpdateShort:
|
|
observeUpdateClass(tracker, accountUserID, value.Update, source)
|
|
case *tg.UpdateShortMessage:
|
|
tracker.observe(value.Message, accountUserID, source)
|
|
}
|
|
}
|
|
|
|
func observeUpdateClasses(tracker *deliveryTracker, accountUserID int64, updates []tg.UpdateClass, source deliverySource) {
|
|
for _, update := range updates {
|
|
observeUpdateClass(tracker, accountUserID, update, source)
|
|
}
|
|
}
|
|
|
|
func observeUpdateClass(tracker *deliveryTracker, accountUserID int64, update tg.UpdateClass, source deliverySource) {
|
|
switch value := update.(type) {
|
|
case *tg.UpdateNewMessage:
|
|
observeMessageClass(tracker, accountUserID, value.Message, source)
|
|
case *tg.UpdateNewChannelMessage:
|
|
observeMessageClass(tracker, accountUserID, value.Message, source)
|
|
}
|
|
}
|
|
|
|
func observeMessageClasses(tracker *deliveryTracker, accountUserID int64, messages []tg.MessageClass, source deliverySource) {
|
|
for _, message := range messages {
|
|
observeMessageClass(tracker, accountUserID, message, source)
|
|
}
|
|
}
|
|
|
|
func observeMessageClass(tracker *deliveryTracker, accountUserID int64, message tg.MessageClass, source deliverySource) {
|
|
if value, ok := message.(*tg.Message); ok {
|
|
tracker.observe(value.Message, accountUserID, source)
|
|
}
|
|
}
|