chromium/chrome/test/data/indexeddb/perf_test.js

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var overallTestStartTime = window.performance.now();
var kUseIndex = true;
var kDontUseIndex = false;
var kReadKeysOnly = true;
var kReadDataToo = false;
var kWriteToo = true;
var kDontWrite = false;
var kWriteSameStore = true;
var kWriteDifferentStore = false;
var kPlaceholderArg = false;
var kDontRead = false;
var kAlternateWithReads = true;

var tests = [
// Create a single small item in a single object store, then delete everything.
  [testCreateAndDeleteDatabase,  1,    1,    1],
// Create many small items in a single object store, then delete everything.
  [testCreateAndDeleteDatabase,  1000,  1,    1],
// Create a single small item in many object stores, then delete everything.
  [testCreateAndDeleteDatabase,  1,    1000,  1],
// Create many large items in a single object store, then delete everything.
  [testCreateAndDeleteDatabase,  1000,    1,  10000],
// Create a single small item in a single object store.
  [testCreateKeysInStores, 1,     1,    1],
// Create many small items in a single object store.
  [testCreateKeysInStores, 1000,   1,    1],
// Create a single small item in many object stores.
  [testCreateKeysInStores, 1,     1000,  1],
// Create many large items in a single object store.
  [testCreateKeysInStores, 1000,   1,    10000],
// Read one item per transaction.
  [testRandomReadsAndWrites, 1000, 1, 0, 1000, kDontUseIndex],
// Read a few random items in each of many transactions.
  [testRandomReadsAndWrites, 1000,  5,    0,  100, kDontUseIndex],
// Read many random items in each of a few transactions.
  [testRandomReadsAndWrites, 1000,  500,   0,  5,  kDontUseIndex],
// Read many random items in each of a few transactions, in a large store.
  [testRandomReadsAndWrites, 10000,  500,   0,  5,  kDontUseIndex],
// Read a few random items from an index, in each of many transactions.
  [testRandomReadsAndWrites, 1000,  5,    0,  100, kUseIndex],
// Read many random items from an index, in each of a few transactions.
  [testRandomReadsAndWrites, 1000,  500,   0,  5,  kUseIndex],
// Read many random items from an index, in each of a few transactions, in a
// large store.
  [testRandomReadsAndWrites, 10000,  500,   0,  5,  kUseIndex],
// Read and write a few random items in each of many transactions.
  [testRandomReadsAndWrites, 1000,  5,    5,  50, kDontUseIndex],
// Read and write a few random items, reading from an index, in each of many
// transactions.
  [testRandomReadsAndWrites, 1000,  5,    5,  50, kUseIndex],
// Read a long, contiguous sequence of an object store via a cursor.
  [testCursorReadsAndRandomWrites, kReadDataToo, kDontUseIndex, kDontWrite,
   kPlaceholderArg],
// Read a sequence of an object store via a cursor, writing
// transformed values into another.
  [testCursorReadsAndRandomWrites, kReadDataToo, kDontUseIndex, kWriteToo,
   kWriteDifferentStore],
// Read a sequence of an object store via a cursor, writing
// transformed values into another.
  [testCursorReadsAndRandomWrites, kReadDataToo, kDontUseIndex, kWriteToo,
   kWriteSameStore],
// Read a sequence of an index into an object store via a cursor.
  [testCursorReadsAndRandomWrites, kReadDataToo, kUseIndex, kDontWrite,
   kPlaceholderArg],
// Read a sequence of an index into an object store via a key cursor.
  [testCursorReadsAndRandomWrites, kReadKeysOnly, kUseIndex, kDontWrite,
   kPlaceholderArg],
// Make a small bunch of batches of reads of the same keys from an object store.
  [testReadCache, 10, kDontUseIndex],
// Make a bunch of batches of reads of the same keys from an index.
  [testReadCache, 50, kUseIndex],
// Make a small bunch of batches of reads of the same keys from an object store.
  [testReadCache, 10, kDontUseIndex],
// Make a bunch of batches of reads of the same keys from an index.
  [testReadCache, 50, kUseIndex],
// Create and delete an index on a store that already contains data [produces
// a timing result for each of creation and deletion].
  [testCreateAndDeleteIndex, 5000],
// Walk through multiple cursors into the same object store, round-robin, until
// you've reached the end of each of them.
  [testWalkingMultipleCursors, 5],
// Walk through many cursors into the same object store, round-robin, until
// you've reached the end of each of them.
  [testWalkingMultipleCursors, 50],
// Open an object store cursor, then continue(key) to the last value.
  [testCursorSeeks, 2000, 10, 4, kDontUseIndex],
// Open an index key cursor, then continue(key) to the last value.
  [testCursorSeeks, 2000, 10, 4, kUseIndex],
];

var currentTest = 0;
var done = false;

function test() {
  runNextTest();
}

var testFilter;

function runNextTest() {
  var filter = testFilter | window.location.hash.slice(1);
  var test, f;
  while (currentTest < tests.length) {
    test = tests[currentTest];
    f = test.shift();
    if (!filter || f.name == filter)
      break;
    ++currentTest;
  }

  if (currentTest < tests.length) {
    test.push(runNextTest);
    f.apply(null, test);
    ++currentTest;
  } else {
    onAllTestsComplete();
  }
}

function onAllTestsComplete() {
  var overallDuration = window.performance.now() - overallTestStartTime;
  automation.addResult("OverallTestDuration", overallDuration);
  automation.setDone();
  done = true;
}

// This is the only test that includes database creation and deletion in its
// results; the others just test specific operations.  To see only the
// creation/deletion without the specific operations used to build up the data
// in the object stores here, subtract off the results of
// testCreateKeysInStores.
function testCreateAndDeleteDatabase(
    numKeys, numStores, payloadLength, onTestComplete) {
  var testName = getDisplayName(arguments);
  assert(numKeys >= 0);
  assert(numStores >= 1);
  var objectStoreNames = [];
  for (var i=0; i < numStores; ++i) {
    objectStoreNames.push("store " + i);
  }
  var value = stringOfLength(payloadLength);
  function getValue() {
    return value;
  }

  automation.setStatus("Creating database.");
  var startTime = window.performance.now();

  createDatabase(testName, objectStoreNames, onCreated, onError);

  function onCreated(db) {
    automation.setStatus("Constructing transaction.");
    var transaction =
        getTransaction(db, objectStoreNames, "readwrite",
            function() { onValuesWritten(db); });
    putLinearValues(transaction, objectStoreNames, numKeys, null, getValue);
  }

  function onValuesWritten(db) {
    automation.setStatus("Deleting database.");
    db.close();
    deleteDatabase(testName, onDeleted);
  }

  function onDeleted() {
    var duration = window.performance.now() - startTime;
    automation.addResult(testName, duration);
    automation.setStatus("Deleted database.");
    onTestComplete();
  }
}

function testCreateKeysInStores(
    numKeys, numStores, payloadLength, onTestComplete) {
  var testName = getDisplayName(arguments);
  assert(numKeys >= 0);
  assert(numStores >= 1);
  var objectStoreNames = [];
  for (var i=0; i < numStores; ++i) {
    objectStoreNames.push("store " + i);
  }
  var value = stringOfLength(payloadLength);
  function getValue() {
    return value;
  }

  automation.setStatus("Creating database.");
  createDatabase(testName, objectStoreNames, onCreated, onError);

  function onCreated(db) {
    automation.setStatus("Constructing transaction.");
    var completionFunc =
        getCompletionFunc(db, testName, window.performance.now(),
            onTestComplete);
    var transaction =
        getTransaction(db, objectStoreNames, "readwrite", completionFunc);
    putLinearValues(transaction, objectStoreNames, numKeys, null, getValue);
  }
}

function testRandomReadsAndWrites(
    numKeys, numReadsPerTransaction, numWritesPerTransaction, numTransactions,
    useIndexForReads, onTestComplete) {
  var indexName;
  if (useIndexForReads)
    indexName = "index";
  var testName = getDisplayName(arguments);
  var objectStoreNames = ["store"];
  var getKey = getSimpleKey;
  var getValue = useIndexForReads ? getIndexableValue : getSimpleValue;

  automation.setStatus("Creating database.");
  var options;
  if (useIndexForReads) {
    options = [{
      indexName: indexName,
      indexKeyPath: "id",
      indexIsUnique: false,
      indexIsMultiEntry: false,
    }];
  }
  createDatabase(testName, objectStoreNames, onCreated, onError, options);

  function onCreated(db) {
    automation.setStatus("Setting up test database.");
    var transaction = getTransaction(db, objectStoreNames, "readwrite",
        function() { onSetupComplete(db); });
    putLinearValues(transaction, objectStoreNames, numKeys, null,
        function() { return "test value"; });
  }

  function onSetupComplete(db) {
    automation.setStatus("Setup complete.");
    var completionFunc =
        getCompletionFunc(db, testName, window.performance.now(),
            onTestComplete);
    var mode = "readonly";
    if (numWritesPerTransaction)
      mode = "readwrite";
    runTransactionBatch(db, numTransactions, batchFunc, objectStoreNames, mode,
        completionFunc);
  }

  function batchFunc(transaction) {
    getRandomValues(transaction, objectStoreNames, numReadsPerTransaction,
        numKeys, indexName, getKey);
    putRandomValues(transaction, objectStoreNames, numWritesPerTransaction,
        numKeys, getKey, getValue);
  }
}

function testReadCache(numTransactions, useIndexForReads, onTestComplete) {
  var numKeys = 10000;
  var numReadsPerTransaction = 50;
  var numTransactionsLeft = numTransactions;
  var indexName;
  if (useIndexForReads)
    indexName = "index";
  var testName = getDisplayName(arguments);
  var objectStoreNames = ["store"];
  var getKey = getSimpleKey;
  var getValue = useIndexForReads ? getIndexableValue : getSimpleValue;
  var keys = [];

  for (var i=0; i < numReadsPerTransaction; ++i) {
    keys.push(getKey(Math.floor(random() * numKeys)));
  }

  automation.setStatus("Creating database.");
  var options;
  if (useIndexForReads) {
    options = [{
      indexName: indexName,
      indexKeyPath: "id",
      indexIsUnique: false,
      indexIsMultiEntry: false,
    }];
  }
  createDatabase(testName, objectStoreNames, onCreated, onError, options);

  function onCreated(db) {
    automation.setStatus("Setting up test database.");
    var transaction = getTransaction(db, objectStoreNames, "readwrite",
        function() { onSetupComplete(db); });
    putLinearValues(transaction, objectStoreNames, numKeys, getKey,
        getValue);
  }

  var completionFunc;
  function onSetupComplete(db) {
    automation.setStatus("Setup complete.");
    completionFunc =
        getCompletionFunc(db, testName, window.performance.now(),
            onTestComplete);
    runTransactionBatch(db, numTransactions, batchFunc, objectStoreNames,
        "readonly", completionFunc);
  }

  function batchFunc(transaction) {
    getSpecificValues(transaction, objectStoreNames, indexName, keys);
  }
}

function testCreateAndDeleteIndex(numKeys, onTestComplete) {
  var testName = getDisplayName(arguments);
  var objectStoreNames = ["store"];

  automation.setStatus("Creating database.");
  createDatabase(testName, objectStoreNames, onCreated, onError);

  var startTime;
  function onCreated(db) {
    automation.setStatus("Initializing data.");
    var transaction = getTransaction(db, objectStoreNames, "readwrite",
        function() { onPopulated(db); });
    putLinearValues(transaction, objectStoreNames, numKeys, null, getValue);
  }

  function getValue(i) {
    return { firstName: i + " first name", lastName: i + " last name" };
  }

  function onPopulated(db) {
    db.close();
    automation.setStatus("Building index.");
    startTime = window.performance.now();
    var f = function(objectStore) {
      objectStore.createIndex("index", "firstName", {unique: true});
    };
    alterObjectStores(testName, objectStoreNames, f, onIndexCreated, onError);
  }

  var indexCreationCompleteTime;
  function onIndexCreated(db) {
    db.close();
    indexCreationCompleteTime = window.performance.now();
    automation.addResult("testCreateIndex",
        indexCreationCompleteTime - startTime);
    var f = function(objectStore) {
      objectStore.deleteIndex("index");
    };
    automation.setStatus("Deleting index.");
    alterObjectStores(testName, objectStoreNames, f, onIndexDeleted, onError);
  }

  function onIndexDeleted(db) {
    var duration = window.performance.now() - indexCreationCompleteTime;
    // Ignore the cleanup time for this test.
    automation.addResult("testDeleteIndex", duration);
    automation.setStatus("Deleting database.");
    db.close();
    deleteDatabase(testName, onDeleted);
  }

  function onDeleted() {
    automation.setStatus("Deleted database.");
    onTestComplete();
  }
}

function testCursorReadsAndRandomWrites(
    readKeysOnly, useIndexForReads, writeAlso, sameStoreForWrites,
    onTestComplete) {
  // There's no key cursor unless you're reading from an index.
  assert(useIndexForReads || !readKeysOnly);
  // If we're writing to another store, having an index would constrain our
  // writes, as we create both object stores with the same configurations.
  // We could do that if needed, but it's simpler not to.
  assert(!useIndexForReads || !writeAlso);
  var numKeys = 10000;
  var numReadsPerTransaction = 1000;
  var testName = getDisplayName(arguments);
  var objectStoreNames = ["input store"];
  var outputStoreName;
  if (writeAlso) {
    if (sameStoreForWrites) {
      outputStoreName = objectStoreNames[0];
    } else {
      outputStoreName = "output store";
      objectStoreNames.push(outputStoreName);
    }
  }
  var getKeyForRead = getSimpleKey;
  var indexName;
  if (useIndexForReads) {
    indexName = "index";
    getKeyForRead = function(i) {
      // This depends on the implementations of getValuesFromCursor and
      // getObjectValue.  We reverse the order of the iteration here so that
      // setting up bounds from k to k+n with n>0 works.  Without this reversal,
      // the upper bound is below the lower bound.
      return getBackwardIndexKey(numKeys - i);
    };
  }

  automation.setStatus("Creating database.");
  var options;
  if (useIndexForReads) {
    options = [{
      indexName: indexName,
      indexKeyPath: "lastName", // depends on getBackwardIndexKey()
      indexIsUnique: true,
      indexIsMultiEntry: false,
    }];
  }
  createDatabase(testName, objectStoreNames, onCreated, onError, options);

  function onCreated(db) {
    automation.setStatus("Setting up test database.");
    var transaction = getTransaction(db, objectStoreNames, "readwrite",
        function() { onSetupComplete(db); });
    putLinearValues(transaction, objectStoreNames, numKeys, getSimpleKey,
        getObjectValue);
  }
  function onSetupComplete(db) {
    automation.setStatus("Setup complete.");
    var completionFunc =
        getCompletionFunc(db, testName, window.performance.now(),
            onTestComplete);
    var mode = "readonly";
    if (writeAlso)
      mode = "readwrite";
    var transaction =
        getTransaction(db, objectStoreNames, mode, completionFunc);

    getValuesFromCursor(
        transaction, objectStoreNames[0], numReadsPerTransaction, numKeys,
        indexName, getKeyForRead, readKeysOnly, outputStoreName);
  }
}

function testWalkingMultipleCursors(numCursors, onTestComplete) {
  var numKeys = 1000;
  var numHitsPerKey = 10;
  var testName = getDisplayName(arguments);
  var objectStoreNames = ["input store"];
  var indexName = "index name";
  var getKey = getSimpleKey;
  var getValue = getIndexableValue;

  automation.setStatus("Creating database.");
  var options = [{
    indexName: indexName,
    indexKeyPath: "id",
    indexIsUnique: false,
    indexIsMultiEntry: false,
  }];
  createDatabase(testName, objectStoreNames, onCreated, onError, options);

  function onCreated(db) {
    automation.setStatus("Setting up test database.");
    var transaction = getTransaction(db, objectStoreNames, "readwrite",
        function() { onSetupComplete(db); });
    // This loop adds the same value numHitsPerKey times for each key.
    for (var i = 0; i < numHitsPerKey; ++i) {
      putLinearValues(transaction, objectStoreNames, numKeys, getKeyFunc(i),
          getValue);
    }
  }
  // While the value is the same each time through the putLinearValues loop, we
  // want the key to keep increaasing for each copy.
  function getKeyFunc(k) {
    return function(i) {
      return getKey(k * numKeys + i);
    };
  }
  var completionFunc;
  function onSetupComplete(db) {
    automation.setStatus("Setup complete.");
    completionFunc =
        getCompletionFunc(db, testName, window.performance.now(),
            onTestComplete);
    var transaction =
        getTransaction(db, objectStoreNames, "readonly", verifyComplete);

    walkSeveralCursors(transaction, numKeys);
  }
  var responseCounts = [];
  var cursorsRunning = numCursors;
  function walkSeveralCursors(transaction, numKeys) {
    var source = transaction.objectStore(objectStoreNames[0]).index(indexName);
    var requests = [];
    var continueCursorIndex = 0;
    for (var i = 0; i < numCursors; ++i) {
      var rand = Math.floor(random() * numKeys);
      // Since we have numHitsPerKey copies of each value in the database,
      // IDBKeyRange.only will return numHitsPerKey results, each referring to a
      // different key with the matching value.
      var request = source.openCursor(IDBKeyRange.only(getSimpleValue(rand)));
      responseCounts.push(0);
      request.onerror = onError;
      request.onsuccess = function(event) {
        assert(cursorsRunning);
        var request = event.target;
        if (!("requestIndex" in request)) {
          assert(requests.length < numCursors);
          request.requestIndex = requests.length;
          requests.push(request);
        }
        var cursor = event.target.result;
        if (cursor) {
          assert(responseCounts[request.requestIndex] < numHitsPerKey);
          ++responseCounts[request.requestIndex];
        } else {
          assert(responseCounts[request.requestIndex] == numHitsPerKey);
          --cursorsRunning;
        }
        if (cursorsRunning) {
          if (requests.length == numCursors) {
            requests[continueCursorIndex++].result.continue();
            continueCursorIndex %= numCursors;
          }
        }
      };
    }
  }
  function verifyComplete() {
    assert(!cursorsRunning);
    completionFunc();
  }
}

function testCursorSeeks(
    numKeys, numSeeksPerTransaction, numTransactions, useIndexForReads,
    onTestComplete) {
  var testName = getDisplayName(arguments);
  var objectStoreNames = ["store"];
  var getKey = useIndexForReads ? getForwardIndexKey : getSimpleKey;
  var indexName;
  if (useIndexForReads) {
    indexName = "index";
  }

  automation.setStatus("Creating database.");
  var options;
  if (useIndexForReads) {
    options = [{
      indexName: indexName,
      indexKeyPath: "firstName",
      indexIsUnique: true,
      indexIsMultiEntry: false,
    }];
  }
  createDatabase(testName, objectStoreNames, onCreated, onError, options);

  function onCreated(db) {
    automation.setStatus("Setting up test database.");
    var transaction = getTransaction(db, objectStoreNames, "readwrite",
        function() { onSetupComplete(db); });
    putLinearValues(transaction, objectStoreNames, numKeys, getSimpleKey,
        getObjectValue);
  }

  function onSetupComplete(db) {
    automation.setStatus("Setup complete.");
    var completionFunc =
          getCompletionFunc(db, testName, window.performance.now(),
              onTestComplete);
    var mode = "readonly";
    runTransactionBatch(db, numTransactions, batchFunc, objectStoreNames, mode,
        completionFunc);
  }

  function batchFunc(transaction) {
    for (var i in objectStoreNames) {
      var source = transaction.objectStore(objectStoreNames[i]);
      if (useIndexForReads)
        source = source.index(indexName);
      for (var j = 0; j < numSeeksPerTransaction; ++j) {
        randomSeek(source);
      }
    }
  }

  function randomSeek(source) {
    var request = useIndexForReads ? source.openKeyCursor()
          : source.openCursor();
    var first = true;
    request.onerror = onError;
    request.onsuccess = function() {
      var cursor = request.result;
      if (cursor && first) {
        first = false;
        cursor.continue(getKey(numKeys - 1));
      }
    };
  }
}