github.com/m3db/m3@v1.5.0/src/query/storage/m3/encoded_step_iterator_test.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package m3
    22  
    23  import (
    24  	"fmt"
    25  	"os"
    26  	"runtime"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/m3db/m3/src/dbnode/encoding"
    32  	"github.com/m3db/m3/src/dbnode/encoding/m3tsz"
    33  	"github.com/m3db/m3/src/dbnode/namespace"
    34  	"github.com/m3db/m3/src/dbnode/ts"
    35  	"github.com/m3db/m3/src/dbnode/x/xio"
    36  	"github.com/m3db/m3/src/query/block"
    37  	"github.com/m3db/m3/src/query/models"
    38  	"github.com/m3db/m3/src/query/pools"
    39  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    40  	"github.com/m3db/m3/src/query/test/compare"
    41  	"github.com/m3db/m3/src/x/checked"
    42  	"github.com/m3db/m3/src/x/ident"
    43  	"github.com/m3db/m3/src/x/pool"
    44  	xsync "github.com/m3db/m3/src/x/sync"
    45  	xtime "github.com/m3db/m3/src/x/time"
    46  
    47  	"github.com/pkg/profile"
    48  	"github.com/stretchr/testify/assert"
    49  	"github.com/stretchr/testify/require"
    50  )
    51  
    52  func withPool(t testing.TB, options Options) Options {
    53  	opts := xsync.
    54  		NewPooledWorkerPoolOptions().
    55  		SetGrowOnDemand(false).
    56  		SetKillWorkerProbability(0).
    57  		SetNumShards(1)
    58  
    59  	readWorkerPools, err := xsync.NewPooledWorkerPool(64, opts)
    60  	require.NoError(t, err)
    61  	readWorkerPools.Init()
    62  
    63  	return options.SetReadWorkerPool(readWorkerPools)
    64  }
    65  
    66  var consolidatedStepIteratorTests = []struct {
    67  	name     string
    68  	stepSize time.Duration
    69  	expected [][]float64
    70  }{
    71  	{
    72  		name:     "1 minute",
    73  		stepSize: time.Minute,
    74  		expected: [][]float64{
    75  			{nan, nan, nan},
    76  			{nan, nan, nan},
    77  			{nan, 10, nan},
    78  			{nan, 20, 100},
    79  			{nan, 20, 100},
    80  			{nan, nan, nan},
    81  			{1, nan, nan},
    82  			{1, nan, nan},
    83  			{3, nan, nan},
    84  			{4, nan, 200},
    85  			{5, nan, 200},
    86  			{6, nan, nan},
    87  			{7, nan, nan},
    88  			{7, 30, nan},
    89  			{nan, 30, nan},
    90  			{nan, nan, 300},
    91  			{nan, nan, 300},
    92  			{8, nan, nan},
    93  			{8, nan, nan},
    94  			{nan, 40, nan},
    95  			{nan, nan, nan},
    96  			{nan, nan, 400},
    97  			{9, nan, 400},
    98  			{9, nan, nan},
    99  			{nan, nan, 500},
   100  			{nan, nan, 500},
   101  			{nan, nan, nan},
   102  			{nan, nan, nan},
   103  			{nan, nan, nan},
   104  			{nan, nan, nan},
   105  		},
   106  	},
   107  	{
   108  		name:     "2 minute",
   109  		stepSize: time.Minute * 2,
   110  		expected: [][]float64{
   111  			{nan, nan, nan},
   112  			{nan, 10, nan},
   113  			{nan, 20, 100},
   114  			{1, nan, nan},
   115  			{3, nan, nan},
   116  			{5, nan, 200},
   117  			{7, nan, nan},
   118  			{nan, 30, nan},
   119  			{nan, nan, 300},
   120  			{8, nan, nan},
   121  			{nan, nan, nan},
   122  			{9, nan, 400},
   123  			{nan, nan, 500},
   124  			{nan, nan, nan},
   125  			{nan, nan, nan},
   126  		},
   127  	},
   128  	{
   129  		name:     "3 minute",
   130  		stepSize: time.Minute * 3,
   131  		expected: [][]float64{
   132  			{nan, nan, nan},
   133  			{nan, 20, 100},
   134  			{1, nan, nan},
   135  			{4, nan, 200},
   136  			{7, nan, nan},
   137  			{nan, nan, 300},
   138  			{8, nan, nan},
   139  			{nan, nan, 400},
   140  			{nan, nan, 500},
   141  			{nan, nan, nan},
   142  		},
   143  	},
   144  }
   145  
   146  func testConsolidatedStepIteratorMinuteLookback(t *testing.T, withPools bool) {
   147  	for _, tt := range consolidatedStepIteratorTests {
   148  		opts := newTestOpts().
   149  			SetLookbackDuration(1 * time.Minute).
   150  			SetSplitSeriesByBlock(false)
   151  		require.NoError(t, opts.Validate())
   152  		if withPools {
   153  			opts = withPool(t, opts)
   154  		}
   155  
   156  		blocks, bounds := generateBlocks(t, tt.stepSize, opts)
   157  		j := 0
   158  		for i, bl := range blocks {
   159  			iters, err := bl.StepIter()
   160  			require.NoError(t, err)
   161  			assert.Equal(t, block.BlockM3TSZCompressed, bl.Info().Type())
   162  
   163  			require.True(t, bounds.Equals(bl.Meta().Bounds))
   164  			verifyMetas(t, i, bl.Meta(), iters.SeriesMeta())
   165  			for iters.Next() {
   166  				step := iters.Current()
   167  				vals := step.Values()
   168  				compare.EqualsWithNans(t, tt.expected[j], vals)
   169  				j++
   170  			}
   171  
   172  			require.NoError(t, iters.Err())
   173  		}
   174  	}
   175  }
   176  
   177  func TestConsolidatedStepIteratorMinuteLookbackParallel(t *testing.T) {
   178  	testConsolidatedStepIteratorMinuteLookback(t, true)
   179  }
   180  
   181  func TestConsolidatedStepIteratorMinuteLookbackSequential(t *testing.T) {
   182  	testConsolidatedStepIteratorMinuteLookback(t, false)
   183  }
   184  
   185  var consolidatedStepIteratorTestsSplitByBlock = []struct {
   186  	name     string
   187  	stepSize time.Duration
   188  	expected [][][]float64
   189  }{
   190  	{
   191  		name:     "1 minute",
   192  		stepSize: time.Minute,
   193  		expected: [][][]float64{
   194  			{
   195  				{nan, nan, nan},
   196  				{nan, nan, nan},
   197  				{nan, 10, nan},
   198  				{nan, 20, 100},
   199  				{nan, nan, nan},
   200  				{nan, nan, nan},
   201  			},
   202  			{
   203  				{1, nan, nan},
   204  				{nan, nan, nan},
   205  				{3, nan, nan},
   206  				{4, nan, 200},
   207  				{5, nan, nan},
   208  				{6, nan, nan},
   209  			},
   210  			{
   211  				{7, nan, nan},
   212  				{nan, nan, nan},
   213  				{nan, nan, nan},
   214  				{nan, nan, 300},
   215  				{nan, nan, nan},
   216  				{8, nan, nan},
   217  			},
   218  			{
   219  				{nan, nan, nan},
   220  				{nan, nan, nan},
   221  				{nan, nan, nan},
   222  				{nan, nan, 400},
   223  				{9, nan, nan},
   224  				{nan, nan, nan},
   225  			},
   226  			{
   227  				{nan, nan, 500},
   228  				{nan, nan, nan},
   229  				{nan, nan, nan},
   230  				{nan, nan, nan},
   231  				{nan, nan, nan},
   232  				{nan, nan, nan},
   233  			},
   234  		},
   235  	},
   236  	{
   237  		name:     "2 minute",
   238  		stepSize: time.Minute * 2,
   239  		expected: [][][]float64{
   240  			{
   241  				{nan, nan, nan},
   242  				{nan, 10, nan},
   243  				{nan, nan, nan},
   244  			},
   245  			{
   246  				{1, nan, nan},
   247  				{3, nan, nan},
   248  				{5, nan, nan},
   249  			},
   250  			{
   251  				{7, nan, nan},
   252  				{nan, nan, nan},
   253  				{nan, nan, nan},
   254  			},
   255  			{
   256  				{nan, nan, nan},
   257  				{nan, nan, nan},
   258  				{9, nan, nan},
   259  			},
   260  			{
   261  				{nan, nan, 500},
   262  				{nan, nan, nan},
   263  				{nan, nan, nan},
   264  			},
   265  		},
   266  	},
   267  	{
   268  		name:     "3 minute",
   269  		stepSize: time.Minute * 3,
   270  		expected: [][][]float64{
   271  			{
   272  				{nan, nan, nan},
   273  				{nan, 20, 100},
   274  			},
   275  			{
   276  				{1, nan, nan},
   277  				{4, nan, 200},
   278  			},
   279  			{
   280  				{7, nan, nan},
   281  				{nan, nan, 300},
   282  			},
   283  			{
   284  				{nan, nan, nan},
   285  				{nan, nan, 400},
   286  			},
   287  			{
   288  				{nan, nan, 500},
   289  				{nan, nan, nan},
   290  			},
   291  		},
   292  	},
   293  }
   294  
   295  func testConsolidatedStepIteratorSplitByBlock(t *testing.T, withPools bool) {
   296  	for _, tt := range consolidatedStepIteratorTestsSplitByBlock {
   297  		opts := newTestOpts().
   298  			SetLookbackDuration(0).
   299  			SetSplitSeriesByBlock(true)
   300  		require.NoError(t, opts.Validate())
   301  		if withPools {
   302  			opts = withPool(t, opts)
   303  		}
   304  
   305  		blocks, bounds := generateBlocks(t, tt.stepSize, opts)
   306  		for i, block := range blocks {
   307  			iters, err := block.StepIter()
   308  			require.NoError(t, err)
   309  
   310  			j := 0
   311  			idx := verifyBoundsAndGetBlockIndex(t, bounds, block.Meta().Bounds)
   312  			verifyMetas(t, i, block.Meta(), iters.SeriesMeta()) // <-
   313  			for iters.Next() {
   314  				step := iters.Current()
   315  				vals := step.Values()
   316  				compare.EqualsWithNans(t, tt.expected[idx][j], vals)
   317  				j++
   318  			}
   319  
   320  			require.NoError(t, iters.Err())
   321  		}
   322  	}
   323  }
   324  
   325  func TestConsolidatedStepIteratorSplitByBlockParallel(t *testing.T) {
   326  	testConsolidatedStepIteratorSplitByBlock(t, true)
   327  }
   328  
   329  func TestConsolidatedStepIteratorSplitByBlockSequential(t *testing.T) {
   330  	testConsolidatedStepIteratorSplitByBlock(t, false)
   331  }
   332  
   333  func benchmarkSingleBlock(b *testing.B, withPools bool) {
   334  	opts := newTestOpts().
   335  		SetLookbackDuration(1 * time.Minute).
   336  		SetSplitSeriesByBlock(false)
   337  	require.NoError(b, opts.Validate())
   338  	if withPools {
   339  		opts = withPool(b, opts)
   340  	}
   341  
   342  	for i := 0; i < b.N; i++ {
   343  		for _, tt := range consolidatedStepIteratorTests {
   344  			b.StopTimer()
   345  			blocks, _ := generateBlocks(b, tt.stepSize, opts)
   346  			b.StartTimer()
   347  			for _, block := range blocks {
   348  				iters, _ := block.StepIter()
   349  				for iters.Next() {
   350  					// no-op
   351  				}
   352  
   353  				require.NoError(b, iters.Err())
   354  			}
   355  		}
   356  	}
   357  }
   358  
   359  func BenchmarkSingleBlockParallel(b *testing.B) {
   360  	benchmarkSingleBlock(b, true)
   361  }
   362  
   363  func BenchmarkSingleBlockSerialll(b *testing.B) {
   364  	benchmarkSingleBlock(b, false)
   365  }
   366  
   367  type noopCollector struct{}
   368  
   369  func (n noopCollector) AddPoint(dp ts.Datapoint) {}
   370  func (n noopCollector) BufferStep()              {}
   371  func (n noopCollector) BufferStepCount() int     { return 0 }
   372  
   373  type iterType uint
   374  
   375  const (
   376  	stepSequential iterType = iota
   377  	stepParallel
   378  	seriesSequential
   379  	seriesBatch
   380  )
   381  
   382  func (t iterType) name(name string) string {
   383  	var n string
   384  	switch t {
   385  	case stepParallel:
   386  		n = "parallel"
   387  	case stepSequential:
   388  		n = "sequential"
   389  	case seriesSequential:
   390  		n = "series"
   391  	case seriesBatch:
   392  		n = "series_batch"
   393  	default:
   394  		panic(fmt.Sprint("bad iter type", t))
   395  	}
   396  
   397  	return fmt.Sprintf("%s_%s", n, name)
   398  }
   399  
   400  type reset func()
   401  type stop func()
   402  
   403  // newTestOptions provides options with very small/non-existent pools
   404  // so that memory profiles don't get cluttered with pooled allocated objects.
   405  func newTestOpts() Options {
   406  	poolOpts := pool.NewObjectPoolOptions().SetSize(1)
   407  	bytesPool := pool.NewCheckedBytesPool(nil, poolOpts,
   408  		func(s []pool.Bucket) pool.BytesPool {
   409  			return pool.NewBytesPool(s, poolOpts)
   410  		})
   411  	bytesPool.Init()
   412  
   413  	iteratorPools := pools.BuildIteratorPools(encoding.NewOptions(),
   414  		pools.BuildIteratorPoolsOptions{
   415  			Replicas:               1,
   416  			SeriesIteratorPoolSize: 1,
   417  			SeriesIteratorsPoolBuckets: []pool.Bucket{
   418  				{Capacity: 1, Count: 1},
   419  			},
   420  			SeriesIDBytesPoolBuckets: []pool.Bucket{
   421  				{Capacity: 1, Count: 1},
   422  			},
   423  			CheckedBytesWrapperPoolSize: 1,
   424  		})
   425  	return newOptions(bytesPool, iteratorPools)
   426  }
   427  
   428  func iterToFetchResult(
   429  	iters []encoding.SeriesIterator,
   430  ) (consolidators.SeriesFetchResult, error) {
   431  	return consolidators.NewSeriesFetchResult(
   432  		encoding.NewSeriesIterators(iters),
   433  		nil,
   434  		block.NewResultMetadata(),
   435  	)
   436  }
   437  
   438  func setupBlock(b *testing.B, iterations int, t iterType) (block.Block, reset, stop) {
   439  	var (
   440  		seriesCount   = 1000
   441  		replicasCount = 3
   442  		start         = xtime.Now()
   443  		stepSize      = time.Second * 10
   444  		window        = stepSize * time.Duration(iterations)
   445  		end           = start.Add(window)
   446  		iters         = make([]encoding.SeriesIterator, seriesCount)
   447  		itersReset    = make([]func(), seriesCount)
   448  
   449  		encodingOpts = encoding.NewOptions()
   450  		namespaceID  = ident.StringID("namespace")
   451  	)
   452  
   453  	for i := 0; i < seriesCount; i++ {
   454  		encoder := m3tsz.NewEncoder(start, checked.NewBytes(nil, nil),
   455  			m3tsz.DefaultIntOptimizationEnabled, encodingOpts)
   456  
   457  		timestamp := start
   458  		for j := 0; j < iterations; j++ {
   459  			timestamp = timestamp.Add(time.Duration(j) * stepSize)
   460  			dp := ts.Datapoint{TimestampNanos: timestamp, Value: float64(j)}
   461  			err := encoder.Encode(dp, xtime.Second, nil)
   462  			require.NoError(b, err)
   463  		}
   464  
   465  		data := encoder.Discard()
   466  		replicas := make([]struct {
   467  			readers []xio.SegmentReader
   468  			iter    encoding.MultiReaderIterator
   469  		}, replicasCount)
   470  		replicasIters := make([]encoding.MultiReaderIterator, replicasCount)
   471  		for j := 0; j < replicasCount; j++ {
   472  			readers := []xio.SegmentReader{xio.NewSegmentReader(data)}
   473  			replicas[j].readers = readers
   474  
   475  			// Use the same decoder over and over to avoid allocations.
   476  			readerIter := m3tsz.NewReaderIterator(nil,
   477  				m3tsz.DefaultIntOptimizationEnabled, encodingOpts)
   478  
   479  			iterAlloc := func(
   480  				r xio.Reader64,
   481  				d namespace.SchemaDescr,
   482  			) encoding.ReaderIterator {
   483  				readerIter.Reset(r, d)
   484  				return readerIter
   485  			}
   486  
   487  			iter := encoding.NewMultiReaderIterator(iterAlloc, nil)
   488  			iter.Reset(readers, start, window, nil)
   489  			replicas[j].iter = iter
   490  
   491  			replicasIters[j] = iter
   492  		}
   493  
   494  		seriesID := ident.StringID(fmt.Sprintf("foo.%d", i+1))
   495  		tags, err := ident.NewTagStringsIterator("foo", "bar", "baz", "qux")
   496  		require.NoError(b, err)
   497  
   498  		iter := encoding.NewSeriesIterator(
   499  			encoding.SeriesIteratorOptions{}, nil)
   500  
   501  		iters[i] = iter
   502  		itersReset[i] = func() {
   503  			// Reset the replica iters.
   504  			for _, replica := range replicas {
   505  				for _, reader := range replica.readers {
   506  					reader.Reset(data)
   507  				}
   508  				replica.iter.Reset(replica.readers, start, window, nil)
   509  			}
   510  			// Reset the series iterator.
   511  			iter.Reset(encoding.SeriesIteratorOptions{
   512  				ID:             seriesID,
   513  				Namespace:      namespaceID,
   514  				Tags:           tags,
   515  				Replicas:       replicasIters,
   516  				StartInclusive: start,
   517  				EndExclusive:   end,
   518  			})
   519  		}
   520  	}
   521  
   522  	usePools := t == stepParallel
   523  
   524  	opts := newTestOpts()
   525  	if usePools {
   526  		poolOpts := xsync.NewPooledWorkerPoolOptions()
   527  		readWorkerPools, err := xsync.NewPooledWorkerPool(1024, poolOpts)
   528  		require.NoError(b, err)
   529  		readWorkerPools.Init()
   530  		opts = opts.SetReadWorkerPool(readWorkerPools)
   531  	}
   532  
   533  	for _, reset := range itersReset {
   534  		reset()
   535  	}
   536  
   537  	res, err := iterToFetchResult(iters)
   538  	require.NoError(b, err)
   539  
   540  	block, err := NewEncodedBlock(
   541  		res,
   542  		models.Bounds{
   543  			Start:    start,
   544  			StepSize: stepSize,
   545  			Duration: window,
   546  		}, false, opts)
   547  
   548  	require.NoError(b, err)
   549  	return block, func() {
   550  			for _, reset := range itersReset {
   551  				reset()
   552  			}
   553  		},
   554  		setupProf(usePools, iterations)
   555  }
   556  
   557  func setupProf(usePools bool, iterations int) stop {
   558  	var prof interface {
   559  		Stop()
   560  	}
   561  	if os.Getenv("PROFILE_TEST_CPU") == "true" {
   562  		key := profileTakenKey{
   563  			profile:    "cpu",
   564  			pools:      usePools,
   565  			iterations: iterations,
   566  		}
   567  		if v := profilesTaken[key]; v == 2 {
   568  			prof = profile.Start(profile.CPUProfile)
   569  		}
   570  
   571  		profilesTaken[key] = profilesTaken[key] + 1
   572  	}
   573  
   574  	if os.Getenv("PROFILE_TEST_MEM") == "true" {
   575  		key := profileTakenKey{
   576  			profile:    "mem",
   577  			pools:      usePools,
   578  			iterations: iterations,
   579  		}
   580  
   581  		if v := profilesTaken[key]; v == 2 {
   582  			prof = profile.Start(profile.MemProfile)
   583  		}
   584  
   585  		profilesTaken[key] = profilesTaken[key] + 1
   586  	}
   587  	return func() {
   588  		if prof != nil {
   589  			prof.Stop()
   590  		}
   591  	}
   592  }
   593  
   594  func benchmarkNextIteration(b *testing.B, iterations int, t iterType) {
   595  	bl, reset, close := setupBlock(b, iterations, t)
   596  	defer close()
   597  
   598  	if t == seriesSequential {
   599  		it, err := bl.SeriesIter()
   600  		require.NoError(b, err)
   601  
   602  		b.ResetTimer()
   603  		for i := 0; i < b.N; i++ {
   604  			reset()
   605  			for it.Next() {
   606  			}
   607  
   608  			require.NoError(b, it.Err())
   609  		}
   610  
   611  		return
   612  	}
   613  
   614  	if t == seriesBatch {
   615  		batches, err := bl.MultiSeriesIter(runtime.GOMAXPROCS(0))
   616  		require.NoError(b, err)
   617  
   618  		var wg sync.WaitGroup
   619  		b.ResetTimer()
   620  		for i := 0; i < b.N; i++ {
   621  			reset()
   622  
   623  			for _, batch := range batches {
   624  				it := batch.Iter
   625  				wg.Add(1)
   626  				go func() {
   627  					for it.Next() {
   628  					}
   629  
   630  					wg.Done()
   631  				}()
   632  			}
   633  
   634  			wg.Wait()
   635  			for _, batch := range batches {
   636  				require.NoError(b, batch.Iter.Err())
   637  			}
   638  		}
   639  
   640  		return
   641  	}
   642  
   643  	it, err := bl.StepIter()
   644  	require.NoError(b, err)
   645  
   646  	b.ResetTimer()
   647  	for i := 0; i < b.N; i++ {
   648  		reset()
   649  		for it.Next() {
   650  		}
   651  
   652  		require.NoError(b, it.Err())
   653  	}
   654  }
   655  
   656  type profileTakenKey struct {
   657  	profile    string
   658  	pools      bool
   659  	iterations int
   660  }
   661  
   662  var (
   663  	profilesTaken = make(map[profileTakenKey]int)
   664  )
   665  
   666  /*
   667  	$ go test -v -run none -bench BenchmarkNextIteration
   668  	goos: darwin
   669  	goarch: amd64
   670  	pkg: github.com/m3db/m3/src/query/ts/m3db
   671  
   672  	BenchmarkNextIteration/sequential_10-12      4112  282491 ns/op
   673  	BenchmarkNextIteration/parallel_10-12        4214  249335 ns/op
   674  	BenchmarkNextIteration/series_10-12          4515  248946 ns/op
   675  	BenchmarkNextIteration/series_batch_10-12    4434  269776 ns/op
   676  
   677  	BenchmarkNextIteration/sequential_100-12     4069  267836 ns/op
   678  	BenchmarkNextIteration/parallel_100-12       4126  283069 ns/op
   679  	BenchmarkNextIteration/series_100-12         4146  266928 ns/op
   680  	BenchmarkNextIteration/series_batch_100-12   4399  255991 ns/op
   681  
   682  	BenchmarkNextIteration/sequential_200-12     4267  245249 ns/op
   683  	BenchmarkNextIteration/parallel_200-12       4233  239597 ns/op
   684  	BenchmarkNextIteration/series_200-12         4365  245924 ns/op
   685  	BenchmarkNextIteration/series_batch_200-12   4485  235055 ns/op
   686  
   687  	BenchmarkNextIteration/sequential_500-12     5108  230085 ns/op
   688  	BenchmarkNextIteration/parallel_500-12       4802  230694 ns/op
   689  	BenchmarkNextIteration/series_500-12         4831  229797 ns/op
   690  	BenchmarkNextIteration/series_batch_500-12   4880  246588 ns/op
   691  
   692  	BenchmarkNextIteration/sequential_1000-12    3807  265449 ns/op
   693  	BenchmarkNextIteration/parallel_1000-12      5062  254942 ns/op
   694  	BenchmarkNextIteration/series_1000-12        4423  236796 ns/op
   695  	BenchmarkNextIteration/series_batch_1000-12  4772  251977 ns/op
   696  
   697  	BenchmarkNextIteration/sequential_2000-12    4916  243593 ns/op
   698  	BenchmarkNextIteration/parallel_2000-12      4743  253677 ns/op
   699  	BenchmarkNextIteration/series_2000-12        4078  256375 ns/op
   700  	BenchmarkNextIteration/series_batch_2000-12  4465  242323 ns/op
   701  */
   702  func BenchmarkNextIteration(b *testing.B) {
   703  	iterTypes := []iterType{
   704  		stepSequential,
   705  		stepParallel,
   706  		seriesSequential,
   707  		seriesBatch,
   708  	}
   709  
   710  	for _, s := range []int{10, 100, 200, 500, 1000, 2000} {
   711  		for _, t := range iterTypes {
   712  			name := t.name(fmt.Sprintf("%d", s))
   713  			b.Run(name, func(b *testing.B) {
   714  				benchmarkNextIteration(b, s, t)
   715  			})
   716  		}
   717  
   718  		// NB: this is for clearer groupings.
   719  		println()
   720  	}
   721  }