github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/rowexec/hashjoiner_test.go (about)

     1  // Copyright 2016 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package rowexec
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"math"
    17  	"sort"
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/cockroachdb/cockroach/pkg/base"
    22  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/execinfra"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/execinfrapb"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    27  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    28  	"github.com/cockroachdb/cockroach/pkg/storage"
    29  	"github.com/cockroachdb/cockroach/pkg/testutils"
    30  	"github.com/cockroachdb/cockroach/pkg/testutils/distsqlutils"
    31  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    32  	"github.com/cockroachdb/cockroach/pkg/util/mon"
    33  	"github.com/cockroachdb/errors"
    34  )
    35  
    36  func TestHashJoiner(t *testing.T) {
    37  	defer leaktest.AfterTest(t)()
    38  
    39  	testCases := joinerTestCases()
    40  
    41  	// Add INTERSECT ALL cases with HashJoinerSpecs.
    42  	for _, tc := range intersectAllTestCases() {
    43  		testCases = append(testCases, setOpTestCaseToJoinerTestCase(tc))
    44  	}
    45  
    46  	// Add EXCEPT ALL cases with HashJoinerSpecs.
    47  	for _, tc := range exceptAllTestCases() {
    48  		testCases = append(testCases, setOpTestCaseToJoinerTestCase(tc))
    49  	}
    50  
    51  	ctx := context.Background()
    52  	st := cluster.MakeTestingClusterSettings()
    53  	tempEngine, _, err := storage.NewTempEngine(ctx, storage.DefaultStorageEngine, base.DefaultTestTempStorageConfig(st), base.DefaultTestStoreSpec)
    54  	if err != nil {
    55  		t.Fatal(err)
    56  	}
    57  	defer tempEngine.Close()
    58  
    59  	evalCtx := tree.MakeTestingEvalContext(st)
    60  	defer evalCtx.Stop(ctx)
    61  	diskMonitor := mon.MakeMonitor(
    62  		"test-disk",
    63  		mon.DiskResource,
    64  		nil, /* curCount */
    65  		nil, /* maxHist */
    66  		-1,  /* increment: use default block size */
    67  		math.MaxInt64,
    68  		st,
    69  	)
    70  	diskMonitor.Start(ctx, nil /* pool */, mon.MakeStandaloneBudget(math.MaxInt64))
    71  	defer diskMonitor.Stop(ctx)
    72  
    73  	for _, c := range testCases {
    74  		// testFunc is a helper function that runs a hashJoin with the current
    75  		// test case.
    76  		// flowCtxSetup can optionally be provided to set up additional testing
    77  		// knobs in the flowCtx before instantiating a hashJoiner and hjSetup can
    78  		// optionally be provided to modify the hashJoiner after instantiation but
    79  		// before Run().
    80  		testFunc := func(t *testing.T, flowCtxSetup func(f *execinfra.FlowCtx), hjSetup func(h *hashJoiner)) error {
    81  			side := rightSide
    82  			for i := 0; i < 2; i++ {
    83  				leftInput := distsqlutils.NewRowBuffer(c.leftTypes, c.leftInput, distsqlutils.RowBufferArgs{})
    84  				rightInput := distsqlutils.NewRowBuffer(c.rightTypes, c.rightInput, distsqlutils.RowBufferArgs{})
    85  				out := &distsqlutils.RowBuffer{}
    86  				flowCtx := execinfra.FlowCtx{
    87  					EvalCtx: &evalCtx,
    88  					Cfg: &execinfra.ServerConfig{
    89  						Settings:    st,
    90  						TempStorage: tempEngine,
    91  						DiskMonitor: &diskMonitor,
    92  					},
    93  				}
    94  				if flowCtxSetup != nil {
    95  					flowCtxSetup(&flowCtx)
    96  				}
    97  				post := execinfrapb.PostProcessSpec{Projection: true, OutputColumns: c.outCols}
    98  				spec := &execinfrapb.HashJoinerSpec{
    99  					LeftEqColumns:  c.leftEqCols,
   100  					RightEqColumns: c.rightEqCols,
   101  					Type:           c.joinType,
   102  					OnExpr:         c.onExpr,
   103  				}
   104  				h, err := newHashJoiner(
   105  					&flowCtx, 0 /* processorID */, spec, leftInput,
   106  					rightInput, &post, out, false, /* disableTempStorage */
   107  				)
   108  				if err != nil {
   109  					return err
   110  				}
   111  				outTypes := h.OutputTypes()
   112  				if hjSetup != nil {
   113  					hjSetup(h)
   114  				}
   115  				// Only force the other side after running the buffering logic once.
   116  				if i == 1 {
   117  					h.forcedStoredSide = &side
   118  				}
   119  				h.Run(context.Background())
   120  				side = otherSide(h.storedSide)
   121  
   122  				if !out.ProducerClosed() {
   123  					return errors.New("output RowReceiver not closed")
   124  				}
   125  
   126  				if err := checkExpectedRows(outTypes, c.expected, out); err != nil {
   127  					return err
   128  				}
   129  			}
   130  			return nil
   131  		}
   132  
   133  		// Run test with a variety of initial buffer sizes.
   134  		for _, initialBuffer := range []int64{0, 32, 64, 128, 1024 * 1024} {
   135  			t.Run(fmt.Sprintf("InitialBuffer=%d", initialBuffer), func(t *testing.T) {
   136  				if err := testFunc(t, nil, func(h *hashJoiner) {
   137  					h.initialBufferSize = initialBuffer
   138  				}); err != nil {
   139  					t.Fatal(err)
   140  				}
   141  			})
   142  		}
   143  
   144  		// Run test with a variety of memory limits.
   145  		for _, memLimit := range []int64{1, 256, 512, 1024, 2048} {
   146  			t.Run(fmt.Sprintf("MemLimit=%d", memLimit), func(t *testing.T) {
   147  				if err := testFunc(t, func(f *execinfra.FlowCtx) {
   148  					f.Cfg.TestingKnobs.MemoryLimitBytes = memLimit
   149  				}, nil); err != nil {
   150  					t.Fatal(err)
   151  				}
   152  			})
   153  		}
   154  	}
   155  }
   156  
   157  func TestHashJoinerError(t *testing.T) {
   158  	defer leaktest.AfterTest(t)()
   159  
   160  	v := [10]sqlbase.EncDatum{}
   161  	for i := range v {
   162  		v[i] = sqlbase.DatumToEncDatum(types.Int, tree.NewDInt(tree.DInt(i)))
   163  	}
   164  
   165  	testCases := joinerErrorTestCases()
   166  
   167  	ctx := context.Background()
   168  	st := cluster.MakeTestingClusterSettings()
   169  	tempEngine, _, err := storage.NewTempEngine(ctx, storage.DefaultStorageEngine, base.DefaultTestTempStorageConfig(st), base.DefaultTestStoreSpec)
   170  	if err != nil {
   171  		t.Fatal(err)
   172  	}
   173  	defer tempEngine.Close()
   174  
   175  	evalCtx := tree.MakeTestingEvalContext(st)
   176  	defer evalCtx.Stop(ctx)
   177  	diskMonitor := mon.MakeMonitor(
   178  		"test-disk",
   179  		mon.DiskResource,
   180  		nil, /* curCount */
   181  		nil, /* maxHist */
   182  		-1,  /* increment: use default block size */
   183  		math.MaxInt64,
   184  		st,
   185  	)
   186  	diskMonitor.Start(ctx, nil /* pool */, mon.MakeStandaloneBudget(math.MaxInt64))
   187  	defer diskMonitor.Stop(ctx)
   188  
   189  	for _, c := range testCases {
   190  		// testFunc is a helper function that runs a hashJoin with the current
   191  		// test case after running the provided setup function.
   192  		testFunc := func(t *testing.T, setup func(h *hashJoiner)) error {
   193  			leftInput := distsqlutils.NewRowBuffer(c.leftTypes, c.leftInput, distsqlutils.RowBufferArgs{})
   194  			rightInput := distsqlutils.NewRowBuffer(c.rightTypes, c.rightInput, distsqlutils.RowBufferArgs{})
   195  			out := &distsqlutils.RowBuffer{}
   196  			flowCtx := execinfra.FlowCtx{
   197  				EvalCtx: &evalCtx,
   198  				Cfg: &execinfra.ServerConfig{
   199  					Settings:    st,
   200  					TempStorage: tempEngine,
   201  					DiskMonitor: &diskMonitor,
   202  				},
   203  			}
   204  
   205  			post := execinfrapb.PostProcessSpec{Projection: true, OutputColumns: c.outCols}
   206  			spec := &execinfrapb.HashJoinerSpec{
   207  				LeftEqColumns:  c.leftEqCols,
   208  				RightEqColumns: c.rightEqCols,
   209  				Type:           c.joinType,
   210  				OnExpr:         c.onExpr,
   211  			}
   212  			h, err := newHashJoiner(
   213  				&flowCtx, 0 /* processorID */, spec, leftInput, rightInput,
   214  				&post, out, false, /* disableTempStorage */
   215  			)
   216  			if err != nil {
   217  				return err
   218  			}
   219  			outTypes := h.OutputTypes()
   220  			setup(h)
   221  			h.Run(context.Background())
   222  
   223  			if !out.ProducerClosed() {
   224  				return errors.New("output RowReceiver not closed")
   225  			}
   226  
   227  			return checkExpectedRows(outTypes, nil, out)
   228  		}
   229  
   230  		t.Run(c.description, func(t *testing.T) {
   231  			if err := testFunc(t, func(h *hashJoiner) {
   232  				h.initialBufferSize = 1024 * 32
   233  			}); err == nil {
   234  				t.Errorf("Expected an error:%s, but found nil", c.expectedErr)
   235  			} else if err.Error() != c.expectedErr.Error() {
   236  				t.Errorf("HashJoinerErrorTest: expected\n%s, but found\n%v", c.expectedErr, err)
   237  			}
   238  		})
   239  	}
   240  }
   241  
   242  func checkExpectedRows(
   243  	types []*types.T, expectedRows sqlbase.EncDatumRows, results *distsqlutils.RowBuffer,
   244  ) error {
   245  	var expected []string
   246  	for _, row := range expectedRows {
   247  		expected = append(expected, row.String(types))
   248  	}
   249  	sort.Strings(expected)
   250  	expStr := strings.Join(expected, "")
   251  
   252  	var rets []string
   253  	for {
   254  		row, meta := results.Next()
   255  		if meta != nil {
   256  			return errors.Errorf("unexpected metadata: %v", meta)
   257  		}
   258  		if row == nil {
   259  			break
   260  		}
   261  		rets = append(rets, row.String(types))
   262  	}
   263  	sort.Strings(rets)
   264  	retStr := strings.Join(rets, "")
   265  
   266  	if expStr != retStr {
   267  		return errors.Errorf("invalid results; expected:\n   %s\ngot:\n   %s",
   268  			expStr, retStr)
   269  	}
   270  	return nil
   271  }
   272  
   273  // TestHashJoinerDrain tests that, if the consumer starts draining, the
   274  // hashJoiner informs the producers and drains them.
   275  //
   276  // Concretely, the HashJoiner is set up to read the right input fully before
   277  // starting to produce rows, so only the left input will be asked to drain if
   278  // the consumer is draining.
   279  func TestHashJoinerDrain(t *testing.T) {
   280  	defer leaktest.AfterTest(t)()
   281  	v := [10]sqlbase.EncDatum{}
   282  	for i := range v {
   283  		v[i] = sqlbase.DatumToEncDatum(types.Int, tree.NewDInt(tree.DInt(i)))
   284  	}
   285  	spec := execinfrapb.HashJoinerSpec{
   286  		LeftEqColumns:  []uint32{0},
   287  		RightEqColumns: []uint32{0},
   288  		Type:           sqlbase.InnerJoin,
   289  		// Implicit @1 = @2 constraint.
   290  	}
   291  	outCols := []uint32{0}
   292  	inputs := []sqlbase.EncDatumRows{
   293  		{
   294  			{v[0]},
   295  			{v[1]},
   296  		},
   297  		{
   298  			{v[0]},
   299  			{v[1]},
   300  		},
   301  	}
   302  	expected := sqlbase.EncDatumRows{
   303  		{v[0]},
   304  	}
   305  	leftInputDrainNotification := make(chan error, 1)
   306  	leftInputConsumerDone := func(rb *distsqlutils.RowBuffer) {
   307  		// Check that draining occurs before the left input has been consumed,
   308  		// not at the end.
   309  		// The left input started with 2 rows and 1 was consumed to find out
   310  		// that we need to drain. So we expect 1 to be left.
   311  		rb.Mu.Lock()
   312  		defer rb.Mu.Unlock()
   313  		if len(rb.Mu.Records) != 1 {
   314  			leftInputDrainNotification <- errors.Errorf(
   315  				"expected 1 row left, got: %d", len(rb.Mu.Records))
   316  			return
   317  		}
   318  		leftInputDrainNotification <- nil
   319  	}
   320  	leftInput := distsqlutils.NewRowBuffer(
   321  		sqlbase.OneIntCol,
   322  		inputs[0],
   323  		distsqlutils.RowBufferArgs{OnConsumerDone: leftInputConsumerDone},
   324  	)
   325  	rightInput := distsqlutils.NewRowBuffer(sqlbase.OneIntCol, inputs[1], distsqlutils.RowBufferArgs{})
   326  	out := distsqlutils.NewRowBuffer(
   327  		sqlbase.OneIntCol,
   328  		nil, /* rows */
   329  		distsqlutils.RowBufferArgs{AccumulateRowsWhileDraining: true},
   330  	)
   331  
   332  	settings := cluster.MakeTestingClusterSettings()
   333  	evalCtx := tree.MakeTestingEvalContext(settings)
   334  	ctx := context.Background()
   335  	defer evalCtx.Stop(ctx)
   336  	flowCtx := execinfra.FlowCtx{
   337  		Cfg:     &execinfra.ServerConfig{Settings: settings},
   338  		EvalCtx: &evalCtx,
   339  	}
   340  
   341  	post := execinfrapb.PostProcessSpec{Projection: true, OutputColumns: outCols}
   342  	// Since the use of external storage overrides h.initialBufferSize, disable
   343  	// it for this test.
   344  	h, err := newHashJoiner(
   345  		&flowCtx, 0 /* processorID */, &spec, leftInput, rightInput,
   346  		&post, out, true, /* disableTempStorage */
   347  	)
   348  	if err != nil {
   349  		t.Fatal(err)
   350  	}
   351  	// Disable initial buffering. We always store the right stream in this case.
   352  	// If not disabled, both streams will be fully consumed before outputting
   353  	// any rows.
   354  	h.initialBufferSize = 0
   355  
   356  	out.ConsumerDone()
   357  	h.Run(context.Background())
   358  
   359  	if !out.ProducerClosed() {
   360  		t.Fatalf("output RowReceiver not closed")
   361  	}
   362  
   363  	callbackErr := <-leftInputDrainNotification
   364  	if callbackErr != nil {
   365  		t.Fatal(callbackErr)
   366  	}
   367  
   368  	leftInput.Mu.Lock()
   369  	defer leftInput.Mu.Unlock()
   370  	if len(leftInput.Mu.Records) != 0 {
   371  		t.Fatalf("left input not drained; still %d rows in it", len(leftInput.Mu.Records))
   372  	}
   373  
   374  	if err := checkExpectedRows(sqlbase.OneIntCol, expected, out); err != nil {
   375  		t.Fatal(err)
   376  	}
   377  }
   378  
   379  // TestHashJoinerDrainAfterBuildPhaseError tests that, if the HashJoiner
   380  // encounters an error in the "build phase" (reading of the right input), the
   381  // joiner will drain both inputs.
   382  func TestHashJoinerDrainAfterBuildPhaseError(t *testing.T) {
   383  	defer leaktest.AfterTest(t)()
   384  
   385  	v := [10]sqlbase.EncDatum{}
   386  	for i := range v {
   387  		v[i] = sqlbase.DatumToEncDatum(types.Int, tree.NewDInt(tree.DInt(i)))
   388  	}
   389  	spec := execinfrapb.HashJoinerSpec{
   390  		LeftEqColumns:  []uint32{0},
   391  		RightEqColumns: []uint32{0},
   392  		Type:           sqlbase.InnerJoin,
   393  		// Implicit @1 = @2 constraint.
   394  	}
   395  	outCols := []uint32{0}
   396  	inputs := []sqlbase.EncDatumRows{
   397  		{
   398  			{v[0]},
   399  			{v[1]},
   400  		},
   401  		{
   402  			{v[0]},
   403  			{v[1]},
   404  		},
   405  	}
   406  	leftInputDrainNotification := make(chan error, 1)
   407  	leftInputConsumerDone := func(rb *distsqlutils.RowBuffer) {
   408  		// Check that draining occurs before the left input has been consumed, not
   409  		// at the end.
   410  		rb.Mu.Lock()
   411  		defer rb.Mu.Unlock()
   412  		if len(rb.Mu.Records) != 2 {
   413  			leftInputDrainNotification <- errors.Errorf(
   414  				"expected 2 rows left in the left input, got: %d", len(rb.Mu.Records))
   415  			return
   416  		}
   417  		leftInputDrainNotification <- nil
   418  	}
   419  	rightInputDrainNotification := make(chan error, 1)
   420  	rightInputConsumerDone := func(rb *distsqlutils.RowBuffer) {
   421  		// Check that draining occurs before the right input has been consumed, not
   422  		// at the end.
   423  		rb.Mu.Lock()
   424  		defer rb.Mu.Unlock()
   425  		if len(rb.Mu.Records) != 2 {
   426  			rightInputDrainNotification <- errors.Errorf(
   427  				"expected 2 rows left in the right input, got: %d", len(rb.Mu.Records))
   428  			return
   429  		}
   430  		rightInputDrainNotification <- nil
   431  	}
   432  	rightErrorReturned := false
   433  	rightInputNext := func(rb *distsqlutils.RowBuffer) (sqlbase.EncDatumRow, *execinfrapb.ProducerMetadata) {
   434  		if !rightErrorReturned {
   435  			rightErrorReturned = true
   436  			// The right input is going to return an error as the first thing.
   437  			return nil, &execinfrapb.ProducerMetadata{Err: errors.Errorf("Test error. Please drain.")}
   438  		}
   439  		// Let RowBuffer.Next() do its usual thing.
   440  		return nil, nil
   441  	}
   442  	leftInput := distsqlutils.NewRowBuffer(
   443  		sqlbase.OneIntCol,
   444  		inputs[0],
   445  		distsqlutils.RowBufferArgs{OnConsumerDone: leftInputConsumerDone},
   446  	)
   447  	rightInput := distsqlutils.NewRowBuffer(
   448  		sqlbase.OneIntCol,
   449  		inputs[1],
   450  		distsqlutils.RowBufferArgs{
   451  			OnConsumerDone: rightInputConsumerDone,
   452  			OnNext:         rightInputNext,
   453  		},
   454  	)
   455  	out := distsqlutils.NewRowBuffer(
   456  		sqlbase.OneIntCol,
   457  		nil, /* rows */
   458  		distsqlutils.RowBufferArgs{},
   459  	)
   460  	st := cluster.MakeTestingClusterSettings()
   461  	evalCtx := tree.MakeTestingEvalContext(st)
   462  	defer evalCtx.Stop(context.Background())
   463  	flowCtx := execinfra.FlowCtx{
   464  		Cfg:     &execinfra.ServerConfig{Settings: st},
   465  		EvalCtx: &evalCtx,
   466  	}
   467  
   468  	// Disable external storage for this test to avoid initializing temp storage
   469  	// infrastructure.
   470  	post := execinfrapb.PostProcessSpec{Projection: true, OutputColumns: outCols}
   471  	h, err := newHashJoiner(
   472  		&flowCtx, 0 /* processorID */, &spec, leftInput, rightInput,
   473  		&post, out, true, /* disableTempStorage */
   474  	)
   475  	if err != nil {
   476  		t.Fatal(err)
   477  	}
   478  	// Disable initial buffering. We always store the right stream in this case.
   479  	h.initialBufferSize = 0
   480  
   481  	h.Run(context.Background())
   482  
   483  	if !out.ProducerClosed() {
   484  		t.Fatalf("output RowReceiver not closed")
   485  	}
   486  
   487  	callbackErr := <-leftInputDrainNotification
   488  	if callbackErr != nil {
   489  		t.Fatal(callbackErr)
   490  	}
   491  
   492  	leftInput.Mu.Lock()
   493  	defer leftInput.Mu.Unlock()
   494  	if len(leftInput.Mu.Records) != 0 {
   495  		t.Fatalf("left input not drained; still %d rows in it", len(leftInput.Mu.Records))
   496  	}
   497  
   498  	out.Mu.Lock()
   499  	defer out.Mu.Unlock()
   500  	if len(out.Mu.Records) != 1 {
   501  		t.Fatalf("expected 1 record, got: %d", len(out.Mu.Records))
   502  	}
   503  	if !testutils.IsError(out.Mu.Records[0].Meta.Err, "Test error. Please drain.") {
   504  		t.Fatalf("expected %q, got: %v", "Test error", out.Mu.Records[0].Meta.Err)
   505  	}
   506  }
   507  
   508  // BenchmarkHashJoiner times how long it takes to join two tables of the same
   509  // variable size. There is a 1:1 relationship between the rows of each table.
   510  // TODO(asubiotto): More complex benchmarks.
   511  func BenchmarkHashJoiner(b *testing.B) {
   512  	ctx := context.Background()
   513  	st := cluster.MakeTestingClusterSettings()
   514  	evalCtx := tree.MakeTestingEvalContext(st)
   515  	defer evalCtx.Stop(ctx)
   516  	diskMonitor := execinfra.NewTestDiskMonitor(ctx, st)
   517  	defer diskMonitor.Stop(ctx)
   518  	flowCtx := &execinfra.FlowCtx{
   519  		EvalCtx: &evalCtx,
   520  		Cfg: &execinfra.ServerConfig{
   521  			Settings:    st,
   522  			DiskMonitor: diskMonitor,
   523  		},
   524  	}
   525  	tempEngine, _, err := storage.NewTempEngine(ctx, storage.DefaultStorageEngine, base.DefaultTestTempStorageConfig(st), base.DefaultTestStoreSpec)
   526  	if err != nil {
   527  		b.Fatal(err)
   528  	}
   529  	defer tempEngine.Close()
   530  	flowCtx.Cfg.TempStorage = tempEngine
   531  
   532  	spec := &execinfrapb.HashJoinerSpec{
   533  		LeftEqColumns:  []uint32{0},
   534  		RightEqColumns: []uint32{0},
   535  		Type:           sqlbase.InnerJoin,
   536  		// Implicit @1 = @2 constraint.
   537  	}
   538  	post := &execinfrapb.PostProcessSpec{}
   539  
   540  	const numCols = 1
   541  	for _, spill := range []bool{true, false} {
   542  		flowCtx.Cfg.TestingKnobs.ForceDiskSpill = spill
   543  		b.Run(fmt.Sprintf("spill=%t", spill), func(b *testing.B) {
   544  			for _, numRows := range []int{0, 1 << 2, 1 << 4, 1 << 8, 1 << 12, 1 << 16} {
   545  				if spill && numRows < 1<<8 {
   546  					// The benchmark takes a long time with a small number of rows and
   547  					// spilling, since the times change wildly. Disable for now.
   548  					continue
   549  				}
   550  				b.Run(fmt.Sprintf("rows=%d", numRows), func(b *testing.B) {
   551  					rows := sqlbase.MakeIntRows(numRows, numCols)
   552  					leftInput := execinfra.NewRepeatableRowSource(sqlbase.OneIntCol, rows)
   553  					rightInput := execinfra.NewRepeatableRowSource(sqlbase.OneIntCol, rows)
   554  					b.SetBytes(int64(8 * numRows * numCols * 2))
   555  					b.ResetTimer()
   556  					for i := 0; i < b.N; i++ {
   557  						// TODO(asubiotto): Get rid of uncleared state between
   558  						// hashJoiner Run()s to omit instantiation time from benchmarks.
   559  						h, err := newHashJoiner(
   560  							flowCtx, 0 /* processorID */, spec, leftInput, rightInput,
   561  							post, &rowDisposer{}, false, /* disableTempStorage */
   562  						)
   563  						if err != nil {
   564  							b.Fatal(err)
   565  						}
   566  						h.Run(context.Background())
   567  						leftInput.Reset()
   568  						rightInput.Reset()
   569  					}
   570  				})
   571  			}
   572  		})
   573  	}
   574  }