Highly-opinionated (ex-bullshit-free) MTPROTO proxy for Telegram. If you use v1.0 or upgrade broke you proxy, please read the chapter Version 2
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

goroutine_test.go 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package benchmarks
  2. import (
  3. "context"
  4. "fmt"
  5. "runtime"
  6. "sync"
  7. "testing"
  8. "time"
  9. )
  10. // stableGoroutineCount returns the current goroutine count after forcing GC
  11. // and giving the runtime a moment to settle.
  12. func stableGoroutineCount() int {
  13. runtime.GC()
  14. runtime.Gosched()
  15. return runtime.NumGoroutine()
  16. }
  17. // memUsage returns StackInuse + HeapAlloc after GC, which gives a stable
  18. // measurement of memory actually consumed by goroutines and their data.
  19. func memUsage() uint64 {
  20. runtime.GC()
  21. runtime.GC() // two passes for more stability
  22. var m runtime.MemStats
  23. runtime.ReadMemStats(&m)
  24. return m.StackInuse + m.HeapAlloc
  25. }
  26. // -------------------------------------------------------
  27. // 1. Memory cost of idle goroutines (blocked on channel)
  28. // -------------------------------------------------------
  29. func TestIdleGoroutineMemory(t *testing.T) {
  30. for _, n := range []int{1000, 2000, 5000, 10000} {
  31. t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
  32. blocker := make(chan struct{})
  33. var wg sync.WaitGroup
  34. // Let runtime settle before measuring
  35. runtime.GC()
  36. time.Sleep(10 * time.Millisecond)
  37. before := memUsage()
  38. goroutinesBefore := runtime.NumGoroutine()
  39. wg.Add(n)
  40. for i := 0; i < n; i++ {
  41. go func() {
  42. wg.Done()
  43. <-blocker
  44. }()
  45. }
  46. wg.Wait() // all goroutines are alive and blocked
  47. after := memUsage()
  48. goroutinesAfter := runtime.NumGoroutine()
  49. spawned := goroutinesAfter - goroutinesBefore
  50. totalBytes := int64(after) - int64(before)
  51. perGoroutine := float64(totalBytes) / float64(spawned)
  52. t.Logf("Spawned %d goroutines (idle, blocked on channel)", spawned)
  53. t.Logf("Total memory delta: %d bytes (%.2f KiB)", totalBytes, float64(totalBytes)/1024)
  54. t.Logf("Per goroutine: %.0f bytes (%.2f KiB)", perGoroutine, perGoroutine/1024)
  55. close(blocker)
  56. runtime.Gosched()
  57. })
  58. }
  59. }
  60. // -------------------------------------------------------
  61. // 2. Memory cost of goroutines with grown stacks
  62. // -------------------------------------------------------
  63. //go:noinline
  64. func growStack(depth int, blocker chan struct{}) {
  65. var buf [1024]byte // 1 KiB per frame
  66. _ = buf
  67. if depth > 0 {
  68. growStack(depth-1, blocker)
  69. return
  70. }
  71. <-blocker
  72. }
  73. func TestGrownStackGoroutineMemory(t *testing.T) {
  74. for _, n := range []int{1000, 2000, 5000} {
  75. t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
  76. blocker := make(chan struct{})
  77. ready := make(chan struct{})
  78. runtime.GC()
  79. time.Sleep(10 * time.Millisecond)
  80. before := memUsage()
  81. for i := 0; i < n; i++ {
  82. go func() {
  83. ready <- struct{}{}
  84. growStack(8, blocker) // ~8 KiB of stack frames
  85. }()
  86. <-ready
  87. }
  88. after := memUsage()
  89. totalBytes := int64(after) - int64(before)
  90. perGoroutine := float64(totalBytes) / float64(n)
  91. t.Logf("Spawned %d goroutines with grown stacks (~8 KiB frames)", n)
  92. t.Logf("Total memory delta: %d bytes (%.2f KiB)", totalBytes, float64(totalBytes)/1024)
  93. t.Logf("Per goroutine: %.0f bytes (%.2f KiB)", perGoroutine, perGoroutine/1024)
  94. close(blocker)
  95. runtime.Gosched()
  96. })
  97. }
  98. }
  99. // -------------------------------------------------------
  100. // 3. Verify context.AfterFunc does NOT spawn goroutines
  101. // until context is cancelled
  102. // -------------------------------------------------------
  103. func TestAfterFuncNoGoroutineUntilCancel(t *testing.T) {
  104. const N = 1000
  105. goroutinesBefore := stableGoroutineCount()
  106. ctxs := make([]context.Context, N)
  107. cancels := make([]context.CancelFunc, N)
  108. stops := make([]func() bool, N)
  109. for i := 0; i < N; i++ {
  110. ctxs[i], cancels[i] = context.WithCancel(context.Background())
  111. stops[i] = context.AfterFunc(ctxs[i], func() {
  112. // noop callback
  113. })
  114. }
  115. goroutinesAfter := stableGoroutineCount()
  116. delta := goroutinesAfter - goroutinesBefore
  117. t.Logf("Registered %d AfterFunc callbacks", N)
  118. t.Logf("Goroutine delta BEFORE cancel: %d (should be 0 or near 0)", delta)
  119. if delta > 5 {
  120. t.Errorf("Expected ~0 extra goroutines before cancel, got %d", delta)
  121. }
  122. // Now cancel all contexts and check goroutines spike momentarily
  123. for i := 0; i < N; i++ {
  124. cancels[i]()
  125. }
  126. runtime.Gosched()
  127. goroutinesPostCancel := runtime.NumGoroutine()
  128. t.Logf("Goroutines right after cancelling %d contexts: %d (baseline was %d)",
  129. N, goroutinesPostCancel, goroutinesBefore)
  130. // Cleanup
  131. _ = stops
  132. }
  133. // -------------------------------------------------------
  134. // 4. Memory comparison: N goroutines vs N AfterFunc
  135. // -------------------------------------------------------
  136. func TestMemoryGoroutinesVsAfterFunc(t *testing.T) {
  137. const N = 5000
  138. // --- Goroutines ---
  139. blocker := make(chan struct{})
  140. var wg sync.WaitGroup
  141. runtime.GC()
  142. time.Sleep(10 * time.Millisecond)
  143. beforeG := memUsage()
  144. wg.Add(N)
  145. for i := 0; i < N; i++ {
  146. ctx, cancel := context.WithCancel(context.Background())
  147. _ = cancel
  148. go func() {
  149. wg.Done()
  150. <-ctx.Done()
  151. }()
  152. }
  153. wg.Wait()
  154. afterG := memUsage()
  155. goroutineMemory := int64(afterG) - int64(beforeG)
  156. close(blocker)
  157. runtime.Gosched()
  158. time.Sleep(10 * time.Millisecond)
  159. // --- AfterFunc ---
  160. runtime.GC()
  161. time.Sleep(10 * time.Millisecond)
  162. beforeAF := memUsage()
  163. cancels := make([]context.CancelFunc, N)
  164. for i := 0; i < N; i++ {
  165. var cancel context.CancelFunc
  166. var ctx context.Context
  167. ctx, cancel = context.WithCancel(context.Background())
  168. cancels[i] = cancel
  169. context.AfterFunc(ctx, func() {})
  170. }
  171. afterAF := memUsage()
  172. afterFuncMemory := int64(afterAF) - int64(beforeAF)
  173. t.Logf("N = %d", N)
  174. t.Logf("Goroutine approach: %d bytes total, %.0f bytes/each", goroutineMemory, float64(goroutineMemory)/N)
  175. t.Logf("AfterFunc approach: %d bytes total, %.0f bytes/each", afterFuncMemory, float64(afterFuncMemory)/N)
  176. if goroutineMemory > 0 {
  177. t.Logf("Memory ratio (goroutine/AfterFunc): %.1fx", float64(goroutineMemory)/float64(afterFuncMemory))
  178. }
  179. // Cleanup
  180. for _, c := range cancels {
  181. c()
  182. }
  183. }
  184. // -------------------------------------------------------
  185. // 5. Benchmark: idle goroutine vs context.AfterFunc
  186. // -------------------------------------------------------
  187. func BenchmarkIdleGoroutine(b *testing.B) {
  188. for i := 0; i < b.N; i++ {
  189. ctx, cancel := context.WithCancel(context.Background())
  190. done := make(chan struct{})
  191. go func() {
  192. <-ctx.Done()
  193. close(done)
  194. }()
  195. cancel()
  196. <-done
  197. }
  198. }
  199. func BenchmarkAfterFunc(b *testing.B) {
  200. for i := 0; i < b.N; i++ {
  201. ctx, cancel := context.WithCancel(context.Background())
  202. done := make(chan struct{})
  203. context.AfterFunc(ctx, func() {
  204. close(done)
  205. })
  206. cancel()
  207. <-done
  208. }
  209. }
  210. // -------------------------------------------------------
  211. // 6. Projection: savings from replacing proxy.go:68-71
  212. // and relay.go:19-23 with context.AfterFunc
  213. // -------------------------------------------------------
  214. func TestProjectedSavings(t *testing.T) {
  215. // Measure per-goroutine cost with large sample
  216. const sampleSize = 5000
  217. blocker := make(chan struct{})
  218. var wg sync.WaitGroup
  219. runtime.GC()
  220. time.Sleep(10 * time.Millisecond)
  221. before := memUsage()
  222. wg.Add(sampleSize)
  223. for i := 0; i < sampleSize; i++ {
  224. go func() {
  225. wg.Done()
  226. <-blocker
  227. }()
  228. }
  229. wg.Wait()
  230. after := memUsage()
  231. close(blocker)
  232. perGoroutine := float64(int64(after)-int64(before)) / float64(sampleSize)
  233. t.Logf("=== Goroutine Audit per Connection ===")
  234. t.Logf("1. proxy.go:68-71 ctx.Done() -> Close() [REPLACEABLE with AfterFunc]")
  235. t.Logf("2. relay.go:19-23 ctx.Done() -> close conns [REPLACEABLE with AfterFunc]")
  236. t.Logf("3. relay.go:27-31 pump (client->telegram) [NOT replaceable, does I/O]")
  237. t.Logf("4. doppel/conn.go:108 clock.Start() [NOT replaceable, timer loop]")
  238. t.Logf("5. doppel/conn.go:111 start() write loop [NOT replaceable, I/O loop]")
  239. t.Logf("")
  240. t.Logf("Total goroutines per connection: 5 (+ ServeConn from ants pool)")
  241. t.Logf("Replaceable with AfterFunc: 2")
  242. t.Logf("")
  243. t.Logf("Measured per-goroutine overhead: %.0f bytes (%.2f KiB)", perGoroutine, perGoroutine/1024)
  244. t.Logf("")
  245. for _, conns := range []int{1000, 2000} {
  246. saved := 2 * conns // 2 goroutines saved per connection
  247. savedBytes := float64(saved) * perGoroutine
  248. t.Logf("At %d connections:", conns)
  249. t.Logf(" Goroutines saved: %d", saved)
  250. t.Logf(" Memory saved: %.2f MiB", savedBytes/1024/1024)
  251. t.Logf(" Remaining goroutines: %d (3 per conn)", 3*conns)
  252. }
  253. t.Logf("")
  254. t.Logf("Note: domain fronting path also spawns relay goroutines,")
  255. t.Logf("but it's an alternative to the telegram relay, not additive.")
  256. }