github.com/haraldrudell/parl@v0.4.176/awaitable-slice.go (about)

     1  /*
     2  © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	"github.com/haraldrudell/parl/pslices"
    13  )
    14  
    15  const (
    16  	// default allocation size for new slices if Size is < 1
    17  	defaultSize = 10
    18  	// scavenging: max size for slice preallocation
    19  	maxForPrealloc = 100
    20  )
    21  
    22  // AwaitableSlice is a queue as thread-safe awaitable unbound slice of element value T or slices of value T
    23  //   - [AwaitableSlice.Send] [AwaitableSlice.Get] allows efficient
    24  //     transfer of single values
    25  //   - [AwaitableSlice.SendSlice] [AwaitableSlice.GetSlice] allows efficient
    26  //     transfer of slices where
    27  //     a sender relinquish slice ownership by invoking SendSlice and
    28  //     a receiving thread gains slice ownership by invoking GetSlice
    29  //   - [AwaitableSlice.DataWaitCh] returns a channel that closes once data is available
    30  //     making the queue awaitable
    31  //   - [AwaitableSlice.EndCh] returns a channel that closes on slice empty,
    32  //     providing close-like behavior
    33  //   - [AwaitableSlice.SetSize] allows for setting initial slice capacity
    34  //   - AwaitableSlice benefits:
    35  //   - — is trouble-free data-sink: non-blocking-unbound-send non-deadlocking panic-free error-free
    36  //   - — is initialization-free awaitable thread-less thread-safe
    37  //   - — features channel-based wait usable with Go select and default
    38  //   - — is unbound configurable low-allocation
    39  //   - — features contention-separation between Send SendSlice and Get GetSlice
    40  //   - — offers high-throughput multiple-value operations SendSlice GetSlice
    41  //   - — avoids temporary large-slice memory leaks by using size
    42  //   - — avoids temporary memory leaks by zero-out of unused slice elements
    43  //   - — although the slice can transfer values almost allocation free or
    44  //     multiple values at a time,
    45  //     the wait mechanic requires pointer allocation 10 ns,
    46  //     channel make 21 ns, channel close 9 ns as well as
    47  //     CAS operations 8/21 ns
    48  //   - compared to Go channel:
    49  //   - — AwaitableSlice is unbound, non-blocking-send error and panic free
    50  //   - — happens-before with each received value or value and close detection
    51  //     similar to unbuffered channel guarantees
    52  //   - — closable by any thread, observable close while also transmitting data
    53  //   - — AwaitableSlice uses closing channel mechanic for wait and close.
    54  //     Data synchronization is [sync.Mutex] and the queue is slice.
    55  //     All is shielded by atomic performance
    56  //   - — for high parallelism, AwaitableSlice has predominately atomic performance while
    57  //     channel has 100× deteriorating unshielded lock performance go1.22.3
    58  //   - AwaitableSlice deprecates:
    59  //   - — [NBChan] fully-featured unbound channel
    60  //   - — [NBRareChan] low-usage unbound channel
    61  //
    62  // Usage:
    63  //
    64  //	var valueQueue parl.AwaitableSlice[*Value]
    65  //	go func(valueSink parl.ValueSink) {
    66  //	  defer valueSink.EmptyCh()
    67  //	  …
    68  //	  valueSink.Send(value)
    69  //	  …
    70  //	}(&valueQueue)
    71  //	endCh := valueQueue.EmptyCh(parl.CloseAwait)
    72  //	for {
    73  //	  select {
    74  //	  case <-valueQueue.DataWaitCh():
    75  //	    for value := valueQueue.Init(); valueQueue.Condition(&value); {
    76  //	      doSomething(value)
    77  //	    }
    78  //	  case <-endCh:
    79  //	    break
    80  //	}
    81  //	// the slice closed
    82  //	…
    83  //	// to reduce blocking, at most 100 at a time
    84  //	for i, hasValue := 0, true; i < 100 && hasValue; i++ {
    85  //	  var value *Value
    86  //	  if value, hasValue = valueQueue.Get(); hasValue {
    87  //	    doSomething(value)
    88  type AwaitableSlice[T any] struct {
    89  	// allocation size for new slices, effective if > 0
    90  	//	- 10 or larger value from SetSize
    91  	size Atomic64[int]
    92  	// maxRetainSize is the longest  slice that will be reused
    93  	//	- avoids temporary memory leaks
    94  	maxRetainSize Atomic64[int]
    95  	// queueLock makes queue and slices thread-safe
    96  	//	- queueLock also makes Send SendSlice critical sections
    97  	queueLock sync.Mutex
    98  	// queue is a locally made slice for individual values
    99  	//	- behind queueLock
   100  	//	- not a slice-away slie
   101  	queue []T
   102  	// slices contains slices of values transferred by SendSlice and
   103  	// possible subsequent locally made slices of values
   104  	//	- slice-away slice, behind queueLock
   105  	slices, slices0 [][]T
   106  	// isLocalSlice is true if the last slice of slices is locally made
   107  	//	- only valid when slices non-empty
   108  	//	- behind queueLock
   109  	isLocalSlice bool
   110  	// indicates at all times whether the queue is empty
   111  	//	- allows for updateDataWait to be invoked without any locks held
   112  	//	- written behind queueLock
   113  	hasData atomic.Bool
   114  	// a pre-allocated slice for queue
   115  	//	- behind queueLock
   116  	//	- allocated by Get Get1 GetAll prior to acquiring queueLock
   117  	cachedInput []T
   118  	// outputLock makes output thread-safe
   119  	//	- outputLock also makes Get1 Get critical sections
   120  	outputLock sync.Mutex
   121  	// output is a slice being sliced away from
   122  	//	- behind outputLock, slice-away slice
   123  	output, output0 []T
   124  	// outputs contains entire-slice values
   125  	//	- behind outputLock, slice-away slice
   126  	outputs, outputs0 [][]T
   127  	// a pre-allocated slice for queue
   128  	//	- behind outputLock
   129  	//	- allocated by Get Get1 GetAll prior to acquiring queueLock
   130  	cachedOutput []T
   131  	// lazy DataWaitCh
   132  	dataWait LazyCyclic
   133  	// lazy emptyWait
   134  	emptyWait LazyCyclic
   135  }
   136  
   137  // Send enqueues a single value. Thread-safe
   138  func (s *AwaitableSlice[T]) Send(value T) {
   139  	defer s.postSend()
   140  	s.queueLock.Lock()
   141  
   142  	// add to queue if no slices
   143  	if len(s.slices) == 0 {
   144  		if s.queue != nil {
   145  			s.queue = append(s.queue, value)
   146  			// create s.queue
   147  		} else if s.cachedInput != nil {
   148  			// use cachedInput allocated under outputLock
   149  			s.queue = append(s.cachedInput, value)
   150  			s.cachedInput = nil
   151  		} else {
   152  			s.queue = s.make(value)
   153  		}
   154  		return // value in queue return
   155  	}
   156  
   157  	// add to slices
   158  	//	- if last slice not locally created, append to new slice
   159  	if s.isLocalSlice {
   160  		// append to ending local slice
   161  		var index = len(s.slices) - 1
   162  		s.slices[index] = append(s.slices[index], value)
   163  		return
   164  	}
   165  
   166  	// append local slice
   167  	var q []T
   168  	if s.cachedInput != nil {
   169  		q = append(s.cachedInput, value)
   170  		s.cachedInput = nil
   171  	} else {
   172  		q = s.make(value)
   173  	}
   174  	pslices.SliceAwayAppend1(&s.slices, &s.slices0, q)
   175  	s.isLocalSlice = true
   176  }
   177  
   178  // SendSlice provides values by transferring ownership of a slice to the queue
   179  //   - SendSlice may reduce allocations and increase performance by handling multiple values
   180  //   - Thread-safe
   181  func (s *AwaitableSlice[T]) SendSlice(values []T) {
   182  	// ignore empty slice
   183  	if len(values) == 0 {
   184  		return
   185  	}
   186  	defer s.postSend()
   187  	s.queueLock.Lock()
   188  
   189  	// append to slices
   190  	s.slices = append(s.slices, values)
   191  	s.isLocalSlice = false
   192  }
   193  
   194  // DataWaitCh returns a channel that closes once values becomes available
   195  //   - Thread-safe
   196  func (s *AwaitableSlice[T]) DataWaitCh() (ch AwaitableCh) {
   197  	// this may initialize the cyclic awaitable
   198  	ch = s.dataWait.Cyclic.Ch()
   199  
   200  	// if previously invoked, no need for initialization
   201  	if s.dataWait.IsActive.Load() {
   202  		return // not first invocation
   203  	}
   204  
   205  	// establish proper state
   206  	//	- data wait ch now in use
   207  	if !s.dataWait.IsActive.CompareAndSwap(false, true) {
   208  		return
   209  	}
   210  
   211  	// set initial state
   212  	s.updateWait()
   213  
   214  	return
   215  }
   216  
   217  // [AwaitableSlice.EmptyCh] initialize: this invocation
   218  // will wait for close-like state, do not activate EmptyCh awaitable
   219  const CloseAwaiter = false
   220  
   221  // EmptyCh returns an awaitable channel that closes on queue being or
   222  // becoming empty
   223  //   - doNotInitialize missing: enable closing of ch which will happen as soon
   224  //     as the slice is empty, possibly prior to return
   225  //   - doNotInitialize CloseAwaiter: obtain the channel but do not enable it closing.
   226  //     A subsequent invocation with doNotInitialize missing will enable its closing thus
   227  //     act as a deferred Close function
   228  //   - thread-safe
   229  func (s *AwaitableSlice[T]) EmptyCh(doNotInitialize ...bool) (ch AwaitableCh) {
   230  	// this may initialize the cyclic awaitable
   231  	ch = s.emptyWait.Cyclic.Ch()
   232  
   233  	// if previously invoked, no need for initialization
   234  	if len(doNotInitialize) > 0 || s.emptyWait.IsActive.Load() {
   235  		return // not first invocation
   236  	}
   237  
   238  	// establish proper state
   239  	//	- data wait ch now in use
   240  	if !s.emptyWait.IsActive.CompareAndSwap(false, true) {
   241  		return
   242  	}
   243  
   244  	// set initial state
   245  	if !s.hasData.Load() {
   246  		s.emptyWait.Cyclic.Close()
   247  	}
   248  
   249  	return
   250  }
   251  
   252  // Get returns one value if the queue is not empty
   253  //   - hasValue true: value is valid
   254  //   - hasValue false: the queue is empty
   255  //   - Get may attain allocation-free receive or allocation-free operation
   256  //   - — a slice is not returned
   257  //   - — an internal slice may be reused reducing allocations
   258  //   - thread-safe
   259  func (s *AwaitableSlice[T]) Get() (value T, hasValue bool) {
   260  	if !s.hasData.Load() {
   261  		return
   262  	}
   263  	defer s.postGet()
   264  	s.outputLock.Lock()
   265  
   266  	// if output empty, transfer outputs[0] to output
   267  	if len(s.output) == 0 && len(s.outputs) > 0 {
   268  		// possibly save output to cachedOutput
   269  		if c := cap(s.output); c == defaultSize && s.cachedOutput == nil {
   270  			s.cachedOutput = s.output0
   271  		}
   272  		// write new s.output
   273  		var so = s.outputs[0]
   274  		s.output = so
   275  		s.output0 = so
   276  		s.outputs[0] = nil
   277  		s.outputs = s.outputs[1:]
   278  	}
   279  
   280  	// try output
   281  	if hasValue = len(s.output) > 0; hasValue {
   282  		value = s.output[0]
   283  		var zeroValue T
   284  		s.output[0] = zeroValue
   285  		s.output = s.output[1:]
   286  		return // got value from s.output
   287  	}
   288  
   289  	// transfer from queueLock
   290  	var slice = s.sliceFromQueue(isOne)
   291  	if hasValue = len(slice) > 0; !hasValue {
   292  		return // no value available
   293  	}
   294  
   295  	// store slice as output and fetch value
   296  	s.output0 = slice
   297  	value = slice[0]
   298  	var zeroValue T
   299  	slice[0] = zeroValue
   300  	s.output = slice[1:]
   301  
   302  	return
   303  }
   304  
   305  // GetSlice returns a slice of values from the queue
   306  //   - values non-nil: a non-empty slice at a time, not necessarily all data.
   307  //     values is never non-nil and empty
   308  //   - — Send-GetSlice: each GetSlice empties the queue
   309  //   - — SendMany-GetSlice: each GetSlice receives one SendMany slice
   310  //   - values nil: the queue is empty
   311  //   - GetSlice may increase performance by slice-at-a-time operation, however,
   312  //     slices need to be allocated:
   313  //   - — Send-GetSlice requires internal slice allocation
   314  //   - — SendMany-GetSlice requires sender to allocate slices
   315  //   - — Send-Get1 may reduce allocations
   316  //   - thread-safe
   317  func (s *AwaitableSlice[T]) GetSlice() (values []T) {
   318  	if !s.hasData.Load() {
   319  		return
   320  	}
   321  	defer s.postGet()
   322  	s.outputLock.Lock()
   323  
   324  	// try output
   325  	if len(s.output) > 0 {
   326  		values = s.output
   327  		s.output0 = nil
   328  		s.output = nil
   329  		return
   330  	}
   331  
   332  	// try s.outputs
   333  	var so = s.outputs
   334  	if len(so) > 0 {
   335  		values = so[0]
   336  		so[0] = nil
   337  		s.outputs = so[1:]
   338  		return
   339  	}
   340  
   341  	// transfer from queueLock
   342  	//	- values may be nil
   343  	values = s.sliceFromQueue(isSlice)
   344  
   345  	return
   346  }
   347  
   348  // GetAll returns a single slice of all unread values in the queue
   349  //   - values nil: the queue is empty
   350  //   - thread-safe
   351  func (s *AwaitableSlice[T]) GetAll() (values []T) {
   352  	if !s.hasData.Load() {
   353  		return
   354  	}
   355  	defer s.postGet()
   356  	s.outputLock.Lock()
   357  
   358  	// aggregate outputLock data
   359  	// output is a copy of s.output since preAlloc may destroy it
   360  	var size = len(s.output)
   361  	for _, o := range s.outputs {
   362  		size += len(o)
   363  	}
   364  
   365  	// aggregate queueLock data
   366  	s.preAlloc(onlyCachedTrue)
   367  	defer s.queueLock.Unlock()
   368  	s.queueLock.Lock()
   369  	s.transferCached()
   370  
   371  	// aggregate queueLock data
   372  	size += len(s.queue)
   373  	for _, s := range s.slices {
   374  		size += len(s)
   375  	}
   376  	// size is now length of the returned slice
   377  
   378  	// no data
   379  	if size == 0 {
   380  		return // no data return
   381  	}
   382  
   383  	// will return all data so queue will be empty
   384  	//	- because size is not zero, hasData is changing
   385  	//	- written while holding queueLock
   386  	s.hasData.Store(false)
   387  
   388  	// attempt allocation-free single slice return
   389  	if values = s.singleSlice(size); len(values) > 0 {
   390  		return // single slice
   391  	}
   392  
   393  	// create aggregate slice
   394  	values = make([]T, 0, size)
   395  	if len(s.output) > 0 {
   396  		values = append(values, s.output...)
   397  		pslices.SetLength(&s.output, 0)
   398  	}
   399  	for _, s := range s.outputs {
   400  		values = append(values, s...)
   401  	}
   402  	pslices.SetLength(&s.outputs, 0)
   403  	if len(s.queue) > 0 {
   404  		values = append(values, s.queue...)
   405  		pslices.SetLength(&s.queue, 0)
   406  	}
   407  	for _, s := range s.slices {
   408  		values = append(values, s...)
   409  	}
   410  	pslices.SetLength(&s.slices, 0)
   411  
   412  	return
   413  }
   414  
   415  // Init allows for AwaitableSlice to be used in a for clause
   416  //   - returns zero-value for a short variable declaration in
   417  //     a for init statement
   418  //   - thread-safe
   419  //
   420  // Usage:
   421  //
   422  //	var a AwaitableSlice[…] = …
   423  //	for value := a.Init(); a.Condition(&value); {
   424  //	  // process received value
   425  //	}
   426  //	// the AwaitableSlice closed
   427  func (s *AwaitableSlice[T]) Init() (value T) { return }
   428  
   429  // Condition allows for AwaitableSlice to be used in a for clause
   430  //   - updates a value variable and returns whether values are present
   431  //   - thread-safe
   432  //
   433  // Usage:
   434  //
   435  //	var a AwaitableSlice[…] = …
   436  //	for value := a.Init(); a.Condition(&value); {
   437  //	  // process received value
   438  //	}
   439  //	// the AwaitableSlice closed
   440  func (s *AwaitableSlice[T]) Condition(valuep *T) (hasValue bool) {
   441  	var endCh AwaitableCh
   442  	for {
   443  
   444  		// try obtaining value
   445  		if s.hasData.Load() {
   446  			var v T
   447  			if v, hasValue = s.Get(); hasValue {
   448  				*valuep = v
   449  				return // value obtained: *valuep valid, hasValue true
   450  			}
   451  			continue
   452  		}
   453  		// hasData is false
   454  		//	- wait until the slice has data or
   455  		//	- the slice closes
   456  
   457  		// atomic-performance check for channel end
   458  		if s.emptyWait.IsActive.Load() {
   459  			// channel is out of items and closed
   460  			return // closed: hasValue false, *valuep unchanged
   461  		}
   462  
   463  		// await data or close
   464  		if endCh == nil {
   465  			// get endCh without initializing close mechanic
   466  			endCh = s.EmptyCh(CloseAwaiter)
   467  		}
   468  		select {
   469  
   470  		// await data, possibly initializing dataWait
   471  		case <-s.DataWaitCh():
   472  
   473  			// await close and end of data
   474  		case <-endCh:
   475  			return // closed: hasValue false, *valuep unchanged
   476  		}
   477  	}
   478  }
   479  
   480  // SetSize set initial allocation size of slices. Thread-safe
   481  func (s *AwaitableSlice[T]) SetSize(size int) {
   482  	var maxSize int
   483  	if size < 1 {
   484  		size = defaultSize
   485  	} else if size > maxForPrealloc {
   486  		maxSize = size
   487  	} else {
   488  		maxSize = maxForPrealloc
   489  	}
   490  	s.size.Store(size)
   491  	s.maxRetainSize.Store(maxSize)
   492  }
   493  
   494  // make returns a new slice of length 0 and configured capacity
   495  //   - value, if present, is added to the new slice
   496  func (s *AwaitableSlice[T]) make(value ...T) (newSlice []T) {
   497  
   498  	// ensure size sizeMax are initialized
   499  	var size = s.size.Load()
   500  	if size == 0 {
   501  		s.SetSize(0)
   502  		size = s.size.Load()
   503  	}
   504  
   505  	//create slice optionally with value
   506  	newSlice = make([]T, len(value), size)
   507  	if len(value) > 0 {
   508  		newSlice[0] = value[0]
   509  	}
   510  
   511  	return
   512  }
   513  
   514  const (
   515  	isOne   = false
   516  	isSlice = true
   517  )
   518  
   519  // sliceFromQueue fetches slices from queue to output
   520  //   - getSlice true: seeking entire slice
   521  //   - getSlice false: seeking single value
   522  //   - invoked when output empty
   523  func (s *AwaitableSlice[T]) sliceFromQueue(getSlice bool) (slice []T) {
   524  	//prealloc outside queueLock
   525  	s.preAlloc()
   526  	s.queueLock.Lock()
   527  	defer s.queueLock.Unlock()
   528  	s.transferCached()
   529  
   530  	// three tasks while holding queueLock:
   531  	//	- find what slice to return
   532  	//	- transfer all other slices to outputLock
   533  	//	- update hasData
   534  
   535  	// retrieve queue if non-empty
   536  	if len(s.queue) > 0 {
   537  		slice = s.queue
   538  		s.queue = nil
   539  	}
   540  	// possibly transfer pre-made output0 to queueLock
   541  	if s.queue == nil {
   542  		// transfer output0 to queueLock
   543  		s.queue = s.output0
   544  		s.output0 = nil
   545  	}
   546  
   547  	// if slice empty, try first of slices
   548  	if len(slice) == 0 && len(s.slices) > 0 {
   549  		slice = s.slices[0]
   550  		s.slices[0] = nil
   551  		s.slices = s.slices[1:]
   552  	}
   553  
   554  	// transfer any remaining slices
   555  	if len(s.slices) > 0 {
   556  		pslices.SliceAwayAppend(&s.outputs, &s.outputs0, s.slices)
   557  		// empty and zero-out s.slices
   558  		pslices.SetLength(&s.slices, 0)
   559  		s.slices = s.slices0[:0]
   560  	}
   561  
   562  	// hasData must be updated while holding queueLock
   563  	//	- it is currently true
   564  	if len(s.outputs) > 0 {
   565  		return // slices in outputs mean data still available: no change
   566  
   567  		// if fetching single value and more than one value in that slice,
   568  		// not end of data
   569  	} else if !getSlice && len(slice) > 1 {
   570  		return // no, single-value fetch and more than one value
   571  	}
   572  
   573  	// the queue is empty: update hasData while holding queueLock
   574  	s.hasData.Store(false)
   575  
   576  	return
   577  }
   578  
   579  // setData updates dataWaitCh if DataWaitCh was invoked
   580  //   - eventually consistent
   581  //   - atomized hasData observation with dataWait emptyWait update
   582  //   - shielded atomic performance
   583  func (s *AwaitableSlice[T]) updateWait() {
   584  
   585  	// dataWait closes on data available, then re-opens
   586  	//	- hasData true: dataWait should be closed
   587  	//	- emptyWait closes on empty and remains closed
   588  	//	- hasData observed false, emptyWait should be closed
   589  
   590  	// is dataWait or emptyWait in use?
   591  	//	- both only go once from false to true
   592  	var dataWait = s.dataWait.IsActive.Load()
   593  
   594  	if dataWait {
   595  		// atomic check based on dataWait
   596  		if s.hasData.Load() == s.dataWait.Cyclic.IsClosed() {
   597  			return // atomically state was ok
   598  		}
   599  		// atomic check based on emptyWait
   600  	} else if !s.emptyWait.IsActive.Load() {
   601  		return // neither is active
   602  		// close emptyCh if empty
   603  	} else if s.hasData.Load() || s.emptyWait.Cyclic.IsClosed() {
   604  		return // atomically state was ok
   605  	}
   606  
   607  	// alter state using the lock of dataWait
   608  	//	- even if dataWait is not active
   609  	//	- atomizes hasData observation with Open/Close operation
   610  	s.dataWait.Lock.Lock()
   611  	defer s.dataWait.Lock.Unlock()
   612  
   613  	// hasData inside lock
   614  	var hasData = s.hasData.Load()
   615  
   616  	// if dataWait active:
   617  	if dataWait {
   618  		// check against dataWait state
   619  		if hasData == s.dataWait.Cyclic.IsClosed() {
   620  			return // no change
   621  		} else if hasData {
   622  			// hasData true: close dataWait
   623  			s.dataWait.Cyclic.Close()
   624  			// emptyWait does not re-open
   625  		} else {
   626  			// hasData false: open dataWait
   627  			s.dataWait.Cyclic.Open()
   628  			// hasData false: trigger emptyWait
   629  			if s.emptyWait.IsActive.Load() {
   630  				s.emptyWait.Cyclic.Close()
   631  			}
   632  			return
   633  		}
   634  	}
   635  	// emptyWait is active, dataWait inactive
   636  
   637  	// if not empty, no action
   638  	if hasData {
   639  		return
   640  	}
   641  	// no data: trigger emptyWait
   642  	s.emptyWait.Cyclic.Close()
   643  }
   644  
   645  // ensureSize ensures that size and maxRetainSize are initialized
   646  //   - size: the configured allocation-size of a new queue slice
   647  func (s *AwaitableSlice[T]) ensureSize() (size int) {
   648  	// ensure size sizeMax are initialized
   649  	if size = s.size.Load(); size == 0 {
   650  		s.SetSize(0)
   651  		size = s.size.Load()
   652  	}
   653  	return
   654  }
   655  
   656  // preAlloc onlyCached
   657  const onlyCachedTrue = true
   658  
   659  // preAlloc ensures that output0 and cachedOutput are allocated
   660  // to configured size
   661  //   - must hold outputLock
   662  func (s *AwaitableSlice[T]) preAlloc(onlyCached ...bool) {
   663  
   664  	var size = s.ensureSize()
   665  	if len(onlyCached) == 0 || !onlyCached[0] {
   666  
   667  		// output0 first pre-allocation
   668  		//	- ensure output0 is a slice of good capacity
   669  		//	- may be transferred to queueLock
   670  		//	- avoids allocation while holding queueLock
   671  		// should output0 be allocated?
   672  		var makeOutput = s.output0 == nil
   673  		if !makeOutput {
   674  			// check capacity of existing output
   675  			var c = cap(s.output0)
   676  			// reuse for capcities defaultSize–maxRetainSize
   677  			makeOutput = c < defaultSize || c > s.maxRetainSize.Load()
   678  		}
   679  		if makeOutput {
   680  			var so = s.make()
   681  			s.output0 = so
   682  			s.output = so
   683  		}
   684  	}
   685  
   686  	// cachedOutput second pre-allocation
   687  	//	- possibly have ready for transfer
   688  	//	- configured size may be large so only for defaultSize
   689  	if s.cachedOutput == nil && size == defaultSize {
   690  		s.cachedOutput = s.make()
   691  	}
   692  }
   693  
   694  // transferCached transfers cachedOutput from
   695  // outputLock to queueLock if possible
   696  //   - invoked while holding outputLock queueLock
   697  func (s *AwaitableSlice[T]) transferCached() {
   698  
   699  	// transfer cachedOutput to queueLock
   700  	if s.cachedInput == nil && s.cachedOutput != nil {
   701  		s.cachedInput = s.cachedOutput
   702  		s.cachedOutput = nil
   703  	}
   704  }
   705  
   706  // postGet relinquishes outputLock and
   707  // initializes eventual update of DataWaitCh and EmptyCh
   708  //   - aggregates deferred actions to reduce latency
   709  //   - invoked while holding outputLock
   710  func (s *AwaitableSlice[T]) postGet() {
   711  	s.outputLock.Unlock()
   712  	s.updateWait()
   713  }
   714  
   715  // singleSlice fetches values if contained in a single slice
   716  //   - reduces slice allocations by using an existing slice
   717  //   - invoked while holding outputLock queueLock
   718  func (s *AwaitableSlice[T]) singleSlice(size int) (values []T) {
   719  
   720  	// only output
   721  	if size == len(s.output) {
   722  		values = s.output
   723  		s.output = nil
   724  		s.output0 = nil
   725  		return // got values
   726  	} else if len(s.output) > 0 {
   727  		return // is aggregate
   728  	}
   729  
   730  	// only outputs[0]
   731  	if len(s.outputs) == 1 && size == len(s.outputs[0]) {
   732  		values = s.outputs[0]
   733  		s.outputs[0] = nil
   734  		s.outputs = s.outputs[1:]
   735  		return // got values
   736  	} else if len(s.outputs) > 0 {
   737  		return // is aggregate
   738  	}
   739  
   740  	// only queue
   741  	if len(s.queue) == size {
   742  		values = s.queue
   743  		s.queue = nil
   744  	}
   745  	// possibly transfer pre-made output0 to queueLock
   746  	if s.queue == nil {
   747  		// transfer output0 to queueLock
   748  		s.queue = s.output0
   749  		s.output0 = nil
   750  	}
   751  	if len(s.queue) > 0 || len(s.queue) == size {
   752  		return // got values or is aggregate
   753  	}
   754  
   755  	// only s.slices[0]
   756  	if len(s.slices) == 1 {
   757  		values = s.slices[0]
   758  		s.slices = s.slices[1:]
   759  	}
   760  
   761  	return // got values or is aggregate
   762  }
   763  
   764  // postSend set hasData true, relinquishes queueuLock and
   765  // initializes eventual update of DataWaitCh and EmptyCh
   766  //   - aggregates deferred actions to reduce latency
   767  //   - invoked while holding queueLock
   768  func (s *AwaitableSlice[T]) postSend() {
   769  	s.hasData.Store(true)
   770  	s.queueLock.Unlock()
   771  	s.updateWait()
   772  }