github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/memory_test/memory_test.go (about) 1 /* 2 * Copyright (c) 2017, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package memory_test 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 "io/ioutil" 27 "os" 28 "runtime" 29 "strings" 30 "sync" 31 "sync/atomic" 32 "testing" 33 "time" 34 35 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon" 36 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 37 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters" 38 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng" 39 ) 40 41 // memory_test is a memory stress test suite that repeatedly reestablishes 42 // tunnels and restarts the Controller. 43 // 44 // runtime.MemStats is used to monitor system memory usage during the test. 45 // 46 // These tests are in its own package as its runtime.MemStats checks must not 47 // be impacted by other test runs. For the same reason, this test doesn't run 48 // a mock server. 49 // 50 // This test is also long-running and _may_ require setting the test flag 51 // "-timeout" beyond the default of 10 minutes (check the testDuration 52 // configured below). Update: testDuration is now reduced from 5 to 2 minutes 53 // since too many iterations -- reconnections -- will impact the ability of 54 // the client to access the network. Manually adjust testDuration to run a 55 // tougher stress test. 56 // 57 // For the most accurate memory reporting, run each test individually; e.g., 58 // go test -run [TestReconnectTunnel|TestRestartController|etc.] 59 60 const ( 61 testModeReconnectTunnel = iota 62 testModeRestartController 63 testModeReconnectAndRestart 64 ) 65 66 func TestReconnectTunnel(t *testing.T) { 67 runMemoryTest(t, testModeReconnectTunnel) 68 } 69 70 func TestRestartController(t *testing.T) { 71 runMemoryTest(t, testModeRestartController) 72 } 73 74 func TestReconnectAndRestart(t *testing.T) { 75 runMemoryTest(t, testModeReconnectAndRestart) 76 } 77 78 func runMemoryTest(t *testing.T, testMode int) { 79 80 testDataDirName, err := ioutil.TempDir("", "psiphon-memory-test") 81 if err != nil { 82 fmt.Printf("TempDir failed: %s\n", err) 83 os.Exit(1) 84 } 85 defer os.RemoveAll(testDataDirName) 86 87 psiphon.SetEmitDiagnosticNotices(true, true) 88 89 configJSON, err := ioutil.ReadFile("../controller_test.config") 90 if err != nil { 91 // Skip, don't fail, if config file is not present 92 t.Skipf("error loading configuration file: %s", err) 93 } 94 95 // Most of these fields _must_ be filled in before calling LoadConfig, 96 // so that they are correctly set into client parameters. 97 var modifyConfig map[string]interface{} 98 json.Unmarshal(configJSON, &modifyConfig) 99 modifyConfig["ClientVersion"] = "999999999" 100 modifyConfig["TunnelPoolSize"] = 1 101 modifyConfig["DataRootDirectory"] = testDataDirName 102 modifyConfig["FetchRemoteServerListRetryPeriodMilliseconds"] = 250 103 modifyConfig["EstablishTunnelPausePeriodSeconds"] = 1 104 modifyConfig["ConnectionWorkerPoolSize"] = 10 105 modifyConfig["DisableLocalSocksProxy"] = true 106 modifyConfig["DisableLocalHTTPProxy"] = true 107 modifyConfig["LimitIntensiveConnectionWorkers"] = 5 108 modifyConfig["LimitMeekBufferSizes"] = true 109 modifyConfig["StaggerConnectionWorkersMilliseconds"] = 100 110 modifyConfig["IgnoreHandshakeStatsRegexps"] = true 111 112 configJSON, _ = json.Marshal(modifyConfig) 113 114 config, err := psiphon.LoadConfig(configJSON) 115 if err != nil { 116 t.Fatalf("error processing configuration file: %s", err) 117 } 118 err = config.Commit(false) 119 if err != nil { 120 t.Fatalf("error committing configuration file: %s", err) 121 } 122 123 // Don't wait for a tactics request. 124 applyParameters := map[string]interface{}{ 125 parameters.TacticsWaitPeriod: "1ms", 126 } 127 err = config.SetParameters("", true, applyParameters) 128 if err != nil { 129 t.Fatalf("SetParameters failed: %s", err) 130 } 131 132 err = psiphon.OpenDataStore(config) 133 if err != nil { 134 t.Fatalf("error initializing datastore: %s", err) 135 } 136 defer psiphon.CloseDataStore() 137 138 var controller *psiphon.Controller 139 var controllerCtx context.Context 140 var controllerStopRunning context.CancelFunc 141 var controllerWaitGroup *sync.WaitGroup 142 restartController := make(chan bool, 1) 143 reconnectTunnel := make(chan bool, 1) 144 tunnelsEstablished := int32(0) 145 146 postActiveTunnelTerminateDelay := 250 * time.Millisecond 147 testDuration := 2 * time.Minute 148 memInspectionFrequency := 10 * time.Second 149 maxInuseBytes := uint64(10 * 1024 * 1024) 150 151 psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver( 152 func(notice []byte) { 153 noticeType, payload, err := psiphon.GetNotice(notice) 154 if err != nil { 155 return 156 } 157 158 switch noticeType { 159 case "Tunnels": 160 count := int(payload["count"].(float64)) 161 if count > 0 { 162 atomic.AddInt32(&tunnelsEstablished, 1) 163 164 time.Sleep(postActiveTunnelTerminateDelay) 165 166 doRestartController := (testMode == testModeRestartController) 167 if testMode == testModeReconnectAndRestart { 168 doRestartController = prng.FlipCoin() 169 } 170 if doRestartController { 171 select { 172 case restartController <- true: 173 default: 174 } 175 } else { 176 select { 177 case reconnectTunnel <- true: 178 default: 179 } 180 } 181 } 182 case "Info": 183 message := payload["message"].(string) 184 if strings.Contains(message, "peak concurrent establish tunnels") { 185 fmt.Printf("%s, ", message) 186 } else if strings.Contains(message, "peak concurrent meek establish tunnels") { 187 fmt.Printf("%s\n", message) 188 } 189 } 190 })) 191 192 startController := func() { 193 controller, err = psiphon.NewController(config) 194 if err != nil { 195 t.Fatalf("error creating controller: %s", err) 196 } 197 198 controllerCtx, controllerStopRunning = context.WithCancel(context.Background()) 199 controllerWaitGroup = new(sync.WaitGroup) 200 201 controllerWaitGroup.Add(1) 202 go func() { 203 defer controllerWaitGroup.Done() 204 controller.Run(controllerCtx) 205 }() 206 } 207 208 stopController := func() { 209 controllerStopRunning() 210 controllerWaitGroup.Wait() 211 } 212 213 testTimer := time.NewTimer(testDuration) 214 defer testTimer.Stop() 215 memInspectionTicker := time.NewTicker(memInspectionFrequency) 216 lastTunnelsEstablished := int32(0) 217 218 startController() 219 220 test_loop: 221 for { 222 select { 223 224 case <-testTimer.C: 225 break test_loop 226 227 case <-memInspectionTicker.C: 228 var m runtime.MemStats 229 runtime.ReadMemStats(&m) 230 inuseBytes := m.HeapInuse + m.StackInuse + m.MSpanInuse + m.MCacheInuse 231 if inuseBytes > maxInuseBytes { 232 t.Fatalf("MemStats.*Inuse bytes exceeds limit: %d", inuseBytes) 233 } else { 234 n := atomic.LoadInt32(&tunnelsEstablished) 235 fmt.Printf("Tunnels established: %d, MemStats.*InUse (peak memory in use): %s, MemStats.TotalAlloc (cumulative allocations): %s\n", 236 n, common.FormatByteCount(inuseBytes), common.FormatByteCount(m.TotalAlloc)) 237 if lastTunnelsEstablished-n >= 0 { 238 t.Fatalf("expected established tunnels") 239 } 240 lastTunnelsEstablished = n 241 } 242 243 case <-reconnectTunnel: 244 controller.TerminateNextActiveTunnel() 245 246 case <-restartController: 247 stopController() 248 startController() 249 } 250 } 251 252 stopController() 253 }