github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/cache/monitor/monitor.go (about)

     1  // Copyright 2018 The WPT Dashboard Project. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package monitor
     6  
     7  import (
     8  	"errors"
     9  	"runtime"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/web-platform-tests/wpt.fyi/api/query/cache/index"
    14  	"github.com/web-platform-tests/wpt.fyi/shared"
    15  )
    16  
    17  var (
    18  	errStopped         = errors.New("Monitor stopped")
    19  	errRunning         = errors.New("Monitor running")
    20  	errNegativePercent = errors.New("Invalid percentage (negative)")
    21  	errPercentTooLarge = errors.New("Invalid percentage (greater than 1.00)")
    22  )
    23  
    24  // Runtime is a wrapper for the go runtime package. It allows tests to mock
    25  // runtime characteristics that code under test may monitor.
    26  type Runtime interface {
    27  	// GetHeapBytes reports the current number of heap allocated bytes.
    28  	GetHeapBytes() uint64
    29  }
    30  
    31  // Monitor is an interface responsible for monitoring runtime conditions.
    32  type Monitor interface {
    33  	// Start starts the monitor, blocking until the monitor stops.
    34  	Start() error
    35  	// Stop stops the monitor.
    36  	Stop() error
    37  	// SetInterval sets the interval at which the monitor polls runtime state.
    38  	SetInterval(time.Duration) error
    39  	// SetMaxHeapBytes sets the limit on heap allocated bytes before attempting to
    40  	// relieve memory pressure.
    41  	SetMaxHeapBytes(uint64) error
    42  	// SetEvictionPercent sets the percentage of runs to be evicted when the soft
    43  	// memory limit (max heap bytes) is reached.
    44  	SetEvictionPercent(float64) error
    45  }
    46  
    47  // ProxyMonitor is a proxy implementation of the Monitor interface. This type is
    48  // generally used in type embeddings that wish to override the behaviour of some
    49  // (but not all) methods, deferring to the delegate for all other behaviours.
    50  type ProxyMonitor struct {
    51  	delegate Monitor
    52  }
    53  
    54  // Start initiates monitoring by deferring to the proxy's delegate.
    55  func (m *ProxyMonitor) Start() error {
    56  	return m.delegate.Start()
    57  }
    58  
    59  // Stop halts monitoring by deferring to the proxy's delegate.
    60  func (m *ProxyMonitor) Stop() error {
    61  	return m.delegate.Stop()
    62  }
    63  
    64  // SetInterval changes the interval at which monitoring operations are performed
    65  // by deferring to the proxy's delegated.
    66  func (m *ProxyMonitor) SetInterval(i time.Duration) error {
    67  	return m.delegate.SetInterval(i)
    68  }
    69  
    70  // SetMaxHeapBytes sets the soft limit on heap memory usage by deferring to the
    71  // proxy's delegated.
    72  func (m *ProxyMonitor) SetMaxHeapBytes(b uint64) error {
    73  	return m.delegate.SetMaxHeapBytes(b)
    74  }
    75  
    76  // SetEvictionPercent sets the percentage of runs to be evicted when the soft
    77  // memory limit (max heap bytes) is reached by deferring to the proxy's
    78  // delegate.
    79  func (m *ProxyMonitor) SetEvictionPercent(percent float64) error {
    80  	return m.delegate.SetEvictionPercent(percent)
    81  }
    82  
    83  // NewProxyMonitor instantiates a new proxy monitor bound to the given delegate.
    84  func NewProxyMonitor(m Monitor) ProxyMonitor {
    85  	return ProxyMonitor{m}
    86  }
    87  
    88  // GoRuntime is the live go runtime implementation of Runtime.
    89  type GoRuntime struct{}
    90  
    91  // GetHeapBytes reports the current number of heap allocated bytes.
    92  func (r GoRuntime) GetHeapBytes() uint64 {
    93  	var stats runtime.MemStats
    94  	runtime.ReadMemStats(&stats)
    95  
    96  	return stats.HeapAlloc
    97  }
    98  
    99  type indexMonitor struct {
   100  	logger          shared.Logger
   101  	rt              Runtime
   102  	interval        time.Duration
   103  	maxIngestedRuns uint
   104  	maxHeapBytes    uint64
   105  	percent         float64
   106  
   107  	isRunning   bool
   108  	mutex       *sync.Mutex
   109  	runIngested chan bool
   110  
   111  	idx index.Index
   112  }
   113  
   114  func (m *indexMonitor) Start() error {
   115  	err := m.start()
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	m.idx.SetIngestChan(m.runIngested)
   121  
   122  	reqs := uint(0)
   123  	for {
   124  		if !m.isRunning {
   125  			return errStopped
   126  		}
   127  
   128  		timer := make(chan bool, 1)
   129  		go func() {
   130  			time.Sleep(m.interval)
   131  			timer <- true
   132  		}()
   133  
   134  		for {
   135  			select {
   136  			case <-timer:
   137  				reqs = 0
   138  			case <-m.runIngested:
   139  				reqs++
   140  				if reqs == m.maxIngestedRuns {
   141  					reqs = 0
   142  				}
   143  			}
   144  			if reqs == 0 {
   145  				break
   146  			}
   147  		}
   148  
   149  		m.check()
   150  	}
   151  }
   152  
   153  func (m *indexMonitor) Stop() error {
   154  	m.mutex.Lock()
   155  	defer m.mutex.Unlock()
   156  	if !m.isRunning {
   157  		return errStopped
   158  	}
   159  	m.isRunning = false
   160  	m.runIngested = make(chan bool)
   161  
   162  	return nil
   163  }
   164  
   165  func (m *indexMonitor) SetInterval(interval time.Duration) error {
   166  	m.mutex.Lock()
   167  	defer m.mutex.Unlock()
   168  	m.interval = interval
   169  
   170  	return nil
   171  }
   172  
   173  func (m *indexMonitor) SetMaxHeapBytes(maxHeapBytes uint64) error {
   174  	m.mutex.Lock()
   175  	defer m.mutex.Unlock()
   176  	m.maxHeapBytes = maxHeapBytes
   177  
   178  	return nil
   179  }
   180  
   181  func (m *indexMonitor) SetEvictionPercent(percent float64) error {
   182  	if percent < 0 {
   183  		return errNegativePercent
   184  	} else if percent > 1.0 {
   185  		return errPercentTooLarge
   186  	}
   187  
   188  	m.percent = percent
   189  
   190  	return nil
   191  }
   192  
   193  func (m *indexMonitor) start() error {
   194  	m.mutex.Lock()
   195  	defer m.mutex.Unlock()
   196  	if m.isRunning {
   197  		return errRunning
   198  	}
   199  	m.isRunning = true
   200  
   201  	return nil
   202  }
   203  
   204  func (m *indexMonitor) check() {
   205  	heapBytes := m.rt.GetHeapBytes()
   206  	if heapBytes > m.maxHeapBytes {
   207  		m.logger.Warningf("Monitor %d bytes allocated, exceeding threshold of %d bytes", heapBytes, m.maxHeapBytes)
   208  		if _, err := m.idx.EvictRuns(m.percent); err != nil {
   209  			m.logger.Warningf("Error occurred while evicting %f%% of current runs: %w", m.percent, err)
   210  		}
   211  	} else {
   212  		m.logger.Debugf("Monitor: %d heap-allocated bytes OK", heapBytes)
   213  	}
   214  }
   215  
   216  // NewIndexMonitor instantiates a new index.Index monitor.
   217  // nolint:ireturn // TODO: Fix ireturn lint error
   218  func NewIndexMonitor(
   219  	logger shared.Logger,
   220  	rt Runtime, interval time.Duration,
   221  	maxIngestedRuns uint,
   222  	maxHeapBytes uint64,
   223  	percent float64,
   224  	idx index.Index,
   225  ) (Monitor, error) {
   226  	if percent < 0 {
   227  		return nil, errNegativePercent
   228  	} else if percent > 1.0 {
   229  		return nil, errPercentTooLarge
   230  	}
   231  
   232  	return &indexMonitor{
   233  		logger,
   234  		rt,
   235  		interval,
   236  		maxIngestedRuns,
   237  		maxHeapBytes,
   238  		percent,
   239  		false,
   240  		&sync.Mutex{},
   241  		make(chan bool),
   242  		idx,
   243  	}, nil
   244  }