Commit 2f547332 authored by Daniel Murphy's avatar Daniel Murphy Committed by Commit Bot

[IndexedDB] Webkit perf benchmark that models a Docs offline IDB load.

See doc for description:
https://docs.google.com/document/d/1JC1RgMyxBAjUPSHjm2Bd1KPzcqpPPvxRomKevOkMPm0/edit

Bug: 736501
Change-Id: I28d61d66948d7ae2403312489cf246247a3f50b3
Reviewed-on: https://chromium-review.googlesource.com/547016
Commit-Queue: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: default avatarNed Nguyen <nednguyen@google.com>
Reviewed-by: default avatarJoshua Bell <jsbell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#486521}
parent 220b856a
# Benchmarks for Open Web Platform Storage.
These benchmarks exercise storage apis in a real-life usage way (avoiding microbenchmarks).
# IDB Docs Load
This models an offline load of a Google doc. See [this document](https://docs.google.com/document/d/1JC1RgMyxBAjUPSHjm2Bd1KPzcqpPPvxRomKevOkMPm0/edit) for a breakdown of the database and the transactions, along with the traces used to extract this information.
\ No newline at end of file
<!doctype html>
<title>IDB Offline Google Docs Load</title>
<script src="../resources/runner.js"></script>
<script src="resources/shared.js"></script>
<script src="resources/idb-load-docs-shared.js"></script>
<script src="resources/idb-load-docs-setup.js"></script>
<script>
deleteThenOpen(databaseName, populateFakeDocsDatabase, () => {
var test = {
description: 'Benchmark modeling the IndexedDB activity of a Google Docs offline page load.',
unit: 'ms',
iterationCount: 20,
tracingCategories: 'IndexedDB',
traceEventsToMeasure: ['IndexedDBTransaction::lifetime', 'IDBObjectStore::delete', 'IDBObjectStore::get', 'IDBObjectStore::put', 'IDBObjectStore::openCursor', 'IDBFactory::open'],
path: "resources/idb-load-docs-runner.html"
}
PerfTestRunner.measurePageLoadTimeAfterDoneMessage(test);
});
</script>
<!doctype html>
<title>IDB Offline Google Docs Load Runner</title>
<div id="content"></div>
<script src="resources/shared.js"></script>
<script src="resources/idb-load-docs-shared.js"></script>
<script>
let openRequest = window.indexedDB.open(databaseName);
openRequest.onsuccess = async () => {
let db = openRequest.result;
// https://docs.google.com/document/d/1JC1RgMyxBAjUPSHjm2Bd1KPzcqpPPvxRomKevOkMPm0/edit
// contiains information about each transaction and their ordering.
let txn1 = () => {
let txn = db.transaction("Users", "readonly");
txn.onabort = reportError;
let users = txn.objectStore("Users");
let get = users.get("ufa1a865c6eb48d97");
get.onsuccess = () => {
logToDocumentBody(["Txn1 got user ", get.result.emailAddress]);
}
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn1 done");
});
};
let txn2 = () => {
let txn = db.transaction(["DocumentLocks"], "readwrite");
txn.onabort = reportError;
let locks = txn.objectStore("DocumentLocks");
let get = locks.get([docId1]);
get.onsuccess = () => {
logToDocumentBody(["Txn2 got lock", get.result.e]);
}
t2_DocumentLocksValues.forEach((value) => (locks.put(value)));
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn2 done.");
});
};
let txn3 = () => {
let txn = db.transaction(["Documents"], "readonly");
txn.onabort = reportError;
let documents = txn.objectStore("Documents");
let get = documents.get(docId1);
get.onsuccess = () => {
var div = logToDocumentBody(
"Txn3 done, got the document, with image: ");
let img = document.createElement("img");
div.appendChild(img);
img.src = get.result.docosKeyData[8][2];
}
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn3 done.");
});
};
let txn4 = () => {
let txn = db.transaction(["PendingQueues"], "readonly");
txn.onabort = reportError;
let queues = txn.objectStore("PendingQueues");
let get = queues.get(docId1);
get.onsuccess = () => {
logToDocumentBody(["Txn4 got queue: ", get.result.r]);
}
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn4 done.");
});
};
let txn5 = () => {
let txn = db.transaction(["SyncObjects"], "readwrite");
txn.onabort = reportError;
let syncs = txn.objectStore("SyncObjects");
t5_SyncObjectsValues.forEach((value) => { syncs.put(value); });
return transactionCompletePromise(txn).then(() => {
logToDocumentBody(
["Txn5 done, set ", t5_SyncObjectsValues.length, " values."]);
});
};
let txn6_15 = () => {
let promises = [];
for (let i = 0; i < 9; ++i) {
let txn = db.transaction("FontMetadata", "readonly");
txn.onabort = reportError;
let get = txn.objectStore("FontMetadata").get("Cambria");
let name = "Txn " + (i + 6);
get.onsuccess = () => {
logToDocumentBody([name, " got font with ",
get.result.fontFaces.length, " faces."]);
}
promises.push(transactionCompletePromise(txn).then(() => {
logToDocumentBody([name, " done."]);
}));
}
return Promise.all(promises);
};
let txn16 = () => {
let txn = db.transaction("BlobMetadata", "readonly");
txn.onabort = reportError;
let request = txn.objectStore("BlobMetadata")
.openCursor(IDBKeyRange.only("test"));
request.onsuccess = function(event) {
logToDocumentBody("Checked blob metadata.");
}
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn16 done.");
});
};
let txn17 = (docGetCallback) => {
let txn = db.transaction(["DocumentLocks", "Documents", "PendingQueues",
"BlobMetadata", "DocumentCommands"],
"readwrite");
txn.onabort = reportError;
let locks = txn.objectStore("DocumentLocks");
let documents = txn.objectStore("Documents");
let documentCommands = txn.objectStore("DocumentCommands");
let queues = txn.objectStore("PendingQueues");
let lockGet;
let documentGet;
let commandsDelete;
let queuesGet;
let gets_done = () => {
logToDocumentBody(["Starting edit, got lock: ", lockGet.result.e]);
let div = logToDocumentBody("Document with picture: ");
let img = document.createElement("img");
div.appendChild(img);
img.src = documentGet.result.docosKeyData[8][2];
logToDocumentBody(["And the queue: ", queuesGet.result.r]);
locks.put(t17_DocumentLocksValue);
documents.put(t17_DocumentsValue);
documentCommands.put(t17_DocumentCommandsValue);
queues.put(t17_PendingQueuesValue);
};
let incrementBarrier = createIncrementalBarrier(gets_done);
lockGet = locks.get([docId1]);
documentGet = documents.get(docId1);
queuesGet = queues.get(docId1);
commandsDelete = documentCommands.delete(
IDBKeyRange.bound([docId1], [docId1, []], false, false));
lockGet.onsuccess = incrementBarrier();
let docGetBarrier = incrementBarrier();
documentGet.onsuccess = () => {
docGetCallback();
docGetBarrier();
};
commandsDelete.onsuccess = incrementBarrier();
queuesGet.onsuccess = incrementBarrier();
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn17 done.");
});
};
let txn18 = () => {
let txn = db.transaction(["Documents"], "readonly");
txn.onabort = reportError;
let documents = txn.objectStore("Documents");
documents.get(docId1).onsuccess = () => {
logToDocumentBody("Got final document!");
}
return transactionCompletePromise(txn).then(() => {
logToDocumentBody("Txn18 done");
});
};
// https://docs.google.com/document/d/1JC1RgMyxBAjUPSHjm2Bd1KPzcqpPPvxRomKevOkMPm0/edit
// has an explanation of transaction order.
await txn1();
await Promise.all([txn2(), txn3(), txn4()]);
await Promise.all([txn5(), txn6_15(), txn16(), txn17(txn18)]);
reportDone();
};
</script>
// Our database schema:
// Users
// * key - string
// * value - dictionary w/ strings and an array
// * 1 entry
// DocumentLocks
// * key - array w/ one string item
// * value - dictionary w/ string, number, and array of string
// * 469 entries
// Documents
// * key - string
// * value - dictionary, w/ nested dictionaries, strings, numbers, arrays
// (one of which has lots of items - numbers, strings, further arrays)
// * 730 entries
// PendingQueues
// * key - string
// * value - dictionary w/ empty array, strings, numbers, bools, undefineds
// * 730 entries
// DocumentCommands
// * key - array
// * value - everything! large.
// * 2000 entries - we only do 20 to make things not too long for startup.
// PendingQueueCommands
// * empty
// SyncObjects
// * key - array of strings
// * value - dictionary w/ dictionaries, keypath, and timestamp
// * 55 entries
// FontMetadata
// * key - string of font name
// * value - dictionary of arrays of dictionaries. strings, numbers, etc
let populateFakeDocsDatabase = function(db) {
function randomAlphaNum(length) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-';
let result = '';
for (let i = length; i > 0; --i)
result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
let otherDocsIds = [];
for (let i = 0; i < 729; i++) {
otherDocsIds.push(randomAlphaNum(44));
}
let populateUser = function() {
let users = db.createObjectStore("Users", {
keyPath: "id"
});
users.put(UsersValue);
}
let populateDocumentLocks = function() {
let documentLocks = db.createObjectStore("DocumentLocks", {
keyPath: "dlKey"
});
documentLocks.put(DocumentLocksValue);
DocumentLocksValue.dlKey = [docId2];
documentLocks.put(DocumentLocksValue);
for (let other_doc_id of otherDocsIds) {
DocumentLocksValue.id = other_doc_id;
documentLocks.put(DocumentLocksValue);
}
}
let populateDocuments = function() {
// first put our 2 docs, then copy the rest.
let documents = db.createObjectStore("Documents", {
keyPath: "id"
});
documents.put(Documents1Value);
documents.put(Documents2Value);
for (let other_doc_id of otherDocsIds) {
Documents2Value.id = other_doc_id;
documents.put(Documents2Value);
}
}
let populateDocumentCommands = function() {
let commands = db.createObjectStore("DocumentCommands", {
keyPath: "dcKey"
});
commands.put(t17_DocumentCommandsValue);
for (let i = 0; i < 50; i++) {
t17_DocumentCommandsValue.dcKey[0] = randomAlphaNum(44);
commands.put(t17_DocumentCommandsValue);
}
}
let populatePendingQueues = function() {
// first put our 2 docs, then copy the rest.
let queues = db.createObjectStore("PendingQueues", {
keyPath: "docId"
});
queues.put(PendingQueues1Value);
queues.put(PendingQueues2Value);
for (let other_doc_id of otherDocsIds) {
PendingQueues2Value.id = other_doc_id;
queues.put(PendingQueues2Value);
}
}
let createPendingQueueCommands = function() {
db.createObjectStore("PendingQueueCommands");
}
let populateSyncObjects = function() {
let sync_objects = db.createObjectStore("SyncObjects", {
keyPath: "keyPath"
});
sync_objects.put(SyncObjects1Value);
sync_objects.put(SyncObjects2Value);
sync_objects.put(SyncObjects3Value);
sync_objects.put(SyncObjects4Value);
for (let i = 0; i < 51; ++i) {
SyncObjects1Value.keyPath[2] = randomAlphaNum(10);
sync_objects.put(SyncObjects4Value);
}
}
let populateFontMetadata = function() {
let fonts = db.createObjectStore("FontMetadata", {
keyPath: "fontFamily"
});
fonts.put(FontMetadata1Value);
fonts.put(FontMetadata2Value);
for (let i = 0; i < 148; ++i) {
FontMetadata1Value.fontFamily = randomAlphaNum(10);
fonts.put(FontMetadata1Value);
}
}
let createBlobMetadata = function() {
db.createObjectStore("BlobMetadata", {
keyPath: ["d", "p"]
});
}
populateUser();
populateDocumentLocks();
populateDocuments();
populateDocumentCommands();
populatePendingQueues();
createPendingQueueCommands();
populateSyncObjects();
populateFontMetadata();
createBlobMetadata();
}
function deleteThenOpen(dbName, upgradeFunc, bodyFunc) {
const deleteRequest = indexedDB.deleteDatabase(dbName);
deleteRequest.onerror = PerfTestRunner.logFatalError.bind('deleteDatabase should not fail');
deleteRequest.onsuccess = (e) => {
const openRequest = indexedDB.open(dbName);
openRequest.onupgradeneeded = () => {
upgradeFunc(openRequest.result, openRequest);
}
openRequest.onsuccess = () => {
bodyFunc(openRequest.result, openRequest);
}
openRequest.onerror = (e) => {
window.PerfTestRunner.logFatalError("Error setting up database " + dbName + ". Error: " + e.type);
}
}
}
// Non-performant on purpose - should cause relayouts.
function logToDocumentBody(stringOrStrings) {
let div = document.createElement("div");
document.body.appendChild(div);
if (Array.isArray(stringOrStrings)) {
for (let string of stringOrStrings) {
div.innerHTML += string;
}
} else {
div.innerHTML = stringOrStrings;
}
return div;
}
function createIncrementalBarrier(callback) {
let count = 0;
let called = false;
return () => {
if (called)
PerfTestRunner.logFatalError("Barrier already used.");
++count;
return () => {
--count;
if (count === 0) {
if (called)
PerfTestRunner.logFatalError("Barrier already used.");
called = true;
callback();
}
}
}
}
function transactionCompletePromise(txn) {
return new Promise((resolve, reject) => {
txn.oncomplete = resolve;
txn.onabort = reject;
});
}
function reportDone() {
window.parent.postMessage({
message: "done"
}, "*");
}
function reportError(event) {
console.log(event);
window.parent.postMessage({
message: "error",
data: event
}, "*", );
}
if (window.PerfTestRunner) {
// The file loaded here will signal a 'done' or 'error' message (see
// reportDone or reportError) which signifies the end of a test run.
window.PerfTestRunner.measurePageLoadTimeAfterDoneMessage = function(test) {
let isDone = false;
let outerDone = test.done;
test.done = (done) => {
isDone = true;
if (outerDone)
done();
}
test.run = () => {
let file = PerfTestRunner.loadFile(test.path);
let runOnce = function(finishedCallback) {
let startTime;
PerfTestRunner.logInfo("Testing " + file.length + " byte document.");
let iframe = document.createElement("iframe");
test.iframe = iframe;
document.body.appendChild(iframe);
iframe.sandbox = '';
// Prevent external loads which could cause write() to return before
// completing the parse.
iframe.style.width = "600px";
// Have a reasonable size so we're not line-breaking on every
// character.
iframe.style.height = "800px";
iframe.contentDocument.open();
let eventHandler = (event)=>{
if (event.data.message == undefined) {
console.log("Unknown message: ", event);
} else if (event.data.message == "done") {
PerfTestRunner.measureValueAsync(PerfTestRunner.now() - startTime);
PerfTestRunner.addRunTestEndMarker();
document.body.removeChild(test.iframe);
finishedCallback();
} else if (event.data.message == "error") {
console.log("Error in page", event.data.data);
PerfTestRunner.logFatalError("error in page: " + event.data.data.type);
} else {
console.log("Unknown message: ", event);
}
window.removeEventListener("message", eventHandler);
}
window.addEventListener("message", eventHandler, false);
PerfTestRunner.addRunTestStartMarker();
startTime = PerfTestRunner.now();
iframe.contentDocument.write(file);
PerfTestRunner.forceLayout(iframe.contentDocument);
iframe.contentDocument.close();
}
let iterationCallback = () => {
if (!isDone)
runOnce(iterationCallback);
}
runOnce(iterationCallback);
}
PerfTestRunner.startMeasureValuesAsync(test)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment