modernc.org/ql@v1.4.7/performance_test.go (about)

     1  package ql_test
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  	"math/rand"
     8  	"os"
     9  	"strconv"
    10  	"sync/atomic"
    11  	"testing"
    12  	"time"
    13  
    14  	_ "modernc.org/ql/driver" // driver
    15  )
    16  
    17  func TestPerformance(t *testing.T) {
    18  	if os.Getenv("PERF") == "" {
    19  		t.Skip("set PERF=1 to run this test")
    20  	}
    21  
    22  	var (
    23  		seed        = envInt(t, "SEED", 123)           // rng seed
    24  		insertCount = envInt(t, "INSERT_COUNT", 10000) // INSERT overall
    25  		batchSize   = envInt(t, "BATCH_SIZE", 128)     // INSERT per transaction
    26  		userCount   = envInt(t, "USER_COUNT", 8)       // unique object users
    27  		idCount     = envInt(t, "ID_COUNT", 64)        // unique object IDs
    28  		kindCount   = envInt(t, "KIND_COUNT", 4)       // unique object kinds
    29  	)
    30  
    31  	rand.Seed(int64(seed))
    32  
    33  	d, err := newDatabase(t)
    34  	if err != nil {
    35  		t.Fatal(err)
    36  	}
    37  
    38  	keys := make([]key, insertCount)
    39  	for i := range keys {
    40  		keys[i] = key{
    41  			user: fmt.Sprintf("user_%d", rand.Intn(userCount)),
    42  			id:   fmt.Sprintf("id_%d", rand.Intn(idCount)),
    43  			kind: fmt.Sprintf("kind_%d", rand.Intn(kindCount)),
    44  		}
    45  	}
    46  
    47  	var (
    48  		begin       = time.Now()
    49  		ctx         = context.Background()
    50  		objects     = map[key]object{}
    51  		batch       = map[key]object{}
    52  		sequence    = uint64(1)
    53  		readCount   = uint64(0)
    54  		writeCount  = uint64(0)
    55  		deleteCount = uint64(0)
    56  	)
    57  
    58  	for _, k := range keys {
    59  		now := time.Now()
    60  
    61  		o := objects[k]
    62  		{
    63  			o.sequence = sequence
    64  			sequence++
    65  
    66  			o.accessCount.Valid = true
    67  			o.accessCount.Int64 = o.accessCount.Int64 + 1
    68  			o.lastAccessAt.Valid = true
    69  			o.lastAccessAt.Time = now
    70  
    71  			r := rand.Float64()
    72  			switch {
    73  			case r < 0.20:
    74  				readCount++
    75  				o.readCount.Valid = true
    76  				o.readCount.Int64 = o.readCount.Int64 + 1
    77  				o.lastReadAt.Valid = true
    78  				o.lastReadAt.Time = now
    79  
    80  			case r < 0.95:
    81  				writeCount++
    82  				o.size.Valid = true
    83  				o.size.Int64 = 100 + int64(rand.Intn(100))
    84  				o.writeCount.Valid = true
    85  				o.writeCount.Int64 = o.writeCount.Int64 + 1
    86  				o.lastWriteAt.Valid = true
    87  				o.lastWriteAt.Time = now
    88  
    89  			default:
    90  				deleteCount++
    91  				o.size.Valid = false
    92  				o.size.Int64 = 0
    93  				o.deleteCount.Valid = true
    94  				o.deleteCount.Int64 = o.deleteCount.Int64 + 1
    95  				o.lastDeleteAt.Valid = true
    96  				o.lastDeleteAt.Time = now
    97  			}
    98  		}
    99  		objects[k] = o
   100  		batch[k] = o
   101  
   102  		if len(batch) >= batchSize {
   103  			if err := d.insertBatch(ctx, batch); err != nil {
   104  				t.Fatal(err)
   105  			}
   106  			batch = map[key]object{}
   107  		}
   108  	}
   109  
   110  	if len(batch) >= 0 {
   111  		if err := d.insertBatch(ctx, batch); err != nil {
   112  			t.Fatal(err)
   113  		}
   114  	}
   115  
   116  	t.Logf("%d INSERT(s) in %s", insertCount, time.Since(begin))
   117  	t.Logf("%d reads, %d writes, %d deletes", readCount, writeCount, deleteCount)
   118  	t.Logf("final object count %d, row count %d", len(objects), d.selectCount(ctx))
   119  }
   120  
   121  //
   122  //
   123  //
   124  
   125  type key struct {
   126  	user string
   127  	id   string
   128  	kind string
   129  }
   130  
   131  type object struct {
   132  	sequence     uint64
   133  	size         sql.NullInt64
   134  	accessCount  sql.NullInt64
   135  	lastAccessAt sql.NullTime
   136  	readCount    sql.NullInt64
   137  	lastReadAt   sql.NullTime
   138  	writeCount   sql.NullInt64
   139  	lastWriteAt  sql.NullTime
   140  	deleteCount  sql.NullInt64
   141  	lastDeleteAt sql.NullTime
   142  }
   143  
   144  //
   145  //
   146  //
   147  
   148  type database struct {
   149  	t   *testing.T
   150  	db  *sql.DB
   151  	ins *sql.Stmt
   152  	upd *sql.Stmt
   153  	sel *sql.Stmt
   154  }
   155  
   156  func newDatabase(t *testing.T) (_ *database, err error) {
   157  	db, err := sql.Open("ql-mem", randomDSN())
   158  	if err != nil {
   159  		return nil, fmt.Errorf("during Open: %w", err)
   160  	}
   161  
   162  	defer func() {
   163  		if err != nil {
   164  			db.Close()
   165  		}
   166  	}()
   167  
   168  	tx, err := db.Begin()
   169  	if err != nil {
   170  		return nil, fmt.Errorf("during Begin: %w", err)
   171  	}
   172  
   173  	if _, err := tx.Exec(`
   174  		CREATE TABLE objects (
   175  			user           string NOT NULL,
   176  			id             string NOT NULL,
   177  			kind           string NOT NULL,
   178  			insert_at      time   NOT NULL DEFAULT timeIn(now(), "UTC"),
   179  			operations     int64  NOT NULL DEFAULT 0,
   180  			sequence       int64  NOT NULL DEFAULT 0,
   181  			size           int64,
   182  			access_count   int64,
   183  			last_access_at time,
   184  			read_count     int64,
   185  			last_read_at   time,
   186  			write_count    int64,
   187  			last_write_at  time,
   188  			delete_count   int64,
   189  			last_delete_at time,
   190  		);
   191  	`); err != nil {
   192  		return nil, fmt.Errorf("during CREATE TABLE: %w", err)
   193  	}
   194  
   195  	for _, s := range []string{
   196  		"CREATE UNIQUE INDEX index_key      ON objects (user, id, kind);",
   197  		"CREATE        INDEX index_access   ON objects (last_access_at);",
   198  		"CREATE        INDEX index_size     ON objects (size);",
   199  		"CREATE        INDEX index_write    ON objects (last_write_at);",
   200  		"CREATE        INDEX index_delete   ON objects (last_delete_at);",
   201  	} {
   202  		if _, err := tx.Exec(s); err != nil {
   203  			return nil, fmt.Errorf("during CREATE INDEX: %w", err)
   204  		}
   205  	}
   206  
   207  	if err := tx.Commit(); err != nil {
   208  		return nil, fmt.Errorf("during Commit: %w", err)
   209  	}
   210  
   211  	ins, err := db.Prepare(`
   212  		INSERT INTO objects
   213  		IF NOT EXISTS
   214  			(user, id, kind)
   215  		VALUES
   216  			($1, $2, $3);
   217  	`)
   218  	if err != nil {
   219  		return nil, fmt.Errorf("during PREPARE for INSERT: %w", err)
   220  	}
   221  
   222  	upd, err := db.Prepare(`
   223  		UPDATE objects
   224  		SET
   225  			operations    = 1 + operations,
   226  			sequence      = $1,
   227  			size          = $2,
   228  			access_count  = $3,
   229  			last_access_at= $4,
   230  			read_count    = $5,
   231  			last_read_at  = $6,
   232  			write_count   = $7,
   233  			last_write_at = $8,
   234  			delete_count  = $9,
   235  			last_delete_at= $10,
   236  		WHERE
   237  			    user == $11
   238  			AND id   == $12
   239  			AND kind == $13
   240  			AND $1 > sequence;
   241  	`)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("during PREPARE for UPDATE: %w", err)
   244  	}
   245  
   246  	sel, err := db.Prepare(`
   247  		SELECT
   248  			user,
   249  			id,
   250  			kind,
   251  			insert_at,
   252  			operations,
   253  			sequence,
   254  			size,
   255  			access_count,
   256  			last_access_at,
   257  			read_count,
   258  			last_read_at,
   259  			write_count,
   260  			last_write_at,
   261  			delete_count,
   262  			last_delete_at,
   263  		FROM objects
   264  		WHERE user == $1 AND id == $2 AND kind == $3;
   265  	`)
   266  	if err != nil {
   267  		return nil, fmt.Errorf("during PREPARE for SELECT: %w", err)
   268  	}
   269  
   270  	return &database{
   271  		t:   t,
   272  		db:  db,
   273  		ins: ins,
   274  		upd: upd,
   275  		sel: sel,
   276  	}, nil
   277  }
   278  
   279  func (d *database) selectCount(ctx context.Context) int {
   280  	var n int
   281  	d.db.QueryRowContext(ctx, `SELECT count() FROM objects;`).Scan(&n)
   282  	return n
   283  }
   284  
   285  func (d *database) insertBatch(ctx context.Context, batch map[key]object) (err error) {
   286  	if len(batch) <= 0 {
   287  		return nil
   288  	}
   289  
   290  	defer func(begin time.Time) {
   291  		var (
   292  			rowCount     = d.selectCount(ctx)
   293  			perBatch     = perUnit(time.Since(begin), 1).Truncate(time.Microsecond)
   294  			perRecord    = perUnit(perBatch, len(batch)).Truncate(time.Microsecond)
   295  			perRecordRow = perUnit(perRecord, rowCount)
   296  		)
   297  		d.t.Helper()
   298  		d.t.Logf(
   299  			"batch size %4d · row count %5d · per batch %10s · per record %10s · per record-row %10s · error %v",
   300  			len(batch), rowCount, perBatch, perRecord, perRecordRow, err,
   301  		)
   302  	}(time.Now())
   303  
   304  	tx, err := d.db.BeginTx(ctx, &sql.TxOptions{})
   305  	if err != nil {
   306  		return err
   307  	}
   308  
   309  	defer func() {
   310  		if err == nil {
   311  			err = tx.Commit()
   312  		} else {
   313  			err = fmt.Errorf("%w (rollback err=%v)", err, tx.Rollback())
   314  		}
   315  	}()
   316  
   317  	var (
   318  		ins = tx.Stmt(d.ins)
   319  		upd = tx.Stmt(d.upd)
   320  	)
   321  	for k, o := range batch {
   322  		if _, err := ins.Exec(
   323  			k.user,
   324  			k.id,
   325  			k.kind,
   326  		); err != nil {
   327  			return fmt.Errorf("INSERT: %w", err)
   328  		}
   329  		if _, err := upd.Exec(
   330  			o.sequence,     // $1
   331  			o.size,         // $2
   332  			o.accessCount,  // $3
   333  			o.lastAccessAt, // $4
   334  			o.readCount,    // $5
   335  			o.lastReadAt,   // $6
   336  			o.writeCount,   // $7
   337  			o.lastWriteAt,  // $8
   338  			o.deleteCount,  // $9
   339  			o.lastDeleteAt, // $10
   340  			k.user,         // $11
   341  			k.id,           // $12
   342  			k.kind,         // $13
   343  		); err != nil {
   344  			return fmt.Errorf("UPDATE: %w", err)
   345  		}
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  //
   352  //
   353  //
   354  
   355  var unique uint64
   356  
   357  func randomDSN() string {
   358  	cnt := atomic.AddUint64(&unique, 1)
   359  	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
   360  	buf := make([]byte, 32)
   361  	n, _ := rng.Read(buf)
   362  	return fmt.Sprintf("memory://%d-%x", cnt, buf[:n])
   363  }
   364  
   365  func perUnit(d time.Duration, n int) time.Duration {
   366  	if n == 0 {
   367  		n = 1
   368  	}
   369  	return d / time.Duration(n)
   370  }
   371  
   372  func envInt(t *testing.T, key string, def int) int {
   373  	t.Helper()
   374  	i, err := strconv.Atoi(os.Getenv(key))
   375  	if err != nil {
   376  		i = def
   377  	}
   378  	t.Logf("%s=%d", key, i)
   379  	return i
   380  }