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  }