From d732610e7fede78df4e9bbc6ce964690d2fe4adf Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sat, 21 Aug 2021 14:48:38 -0700 Subject: [PATCH] perf: use relaxed IDB transactions and manually commit (#218) * perf: use relaxed IDB transactions and manually commit * fix: reduce time spent in benchmark * fix: add missing txn.commit() * fix: small syntax change --- .github/workflows/benchmarks.yml | 16 +++++++ src/database/databaseLifecycle.js | 14 +++--- src/database/idbInterface.js | 31 +++++++------ src/database/idbUtil.js | 7 +++ test/benchmark/benchmark.js | 2 +- test/benchmark/database-interactions.html | 31 +++++++++++++ .../database-interactions.tachometer.json | 43 +++++++++++++++++++ 7 files changed, 123 insertions(+), 21 deletions(-) create mode 100644 test/benchmark/database-interactions.html create mode 100644 test/benchmark/database-interactions.tachometer.json diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 32f6ddd5..c7b79f9d 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -73,3 +73,19 @@ jobs: path: test/benchmark/second-load.results.json pr-bench-name: this-change base-bench-name: tip-of-tree + + # database-interactions + - name: Benchmark database-interactions + run: | + cd test/benchmark + ../../node_modules/.bin/tach \ + --config ./database-interactions.tachometer.json \ + --json-file ./database-interactions.results.json + + - name: Report database-interactions + uses: andrewiggins/tachometer-reporter-action@v2 + with: + report-id: emoji-picker-element-database-interactions + path: test/benchmark/database-interactions.results.json + pr-bench-name: this-change + base-bench-name: tip-of-tree \ No newline at end of file diff --git a/src/database/databaseLifecycle.js b/src/database/databaseLifecycle.js index 38dc5acd..58d08f8c 100644 --- a/src/database/databaseLifecycle.js +++ b/src/database/databaseLifecycle.js @@ -50,18 +50,20 @@ export function openDatabase (dbName) { export function dbPromise (db, storeName, readOnlyOrReadWrite, cb) { return new Promise((resolve, reject) => { - const tx = db.transaction(storeName, readOnlyOrReadWrite) + // Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone + // are really irreplaceable data. IndexedDB is just a cache in this case. + const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' }) const store = typeof storeName === 'string' - ? tx.objectStore(storeName) - : storeName.map(name => tx.objectStore(name)) + ? txn.objectStore(storeName) + : storeName.map(name => txn.objectStore(name)) let res - cb(store, (result) => { + cb(store, txn, (result) => { res = result }) - tx.oncomplete = () => resolve(res) + txn.oncomplete = () => resolve(res) /* istanbul ignore next */ - tx.onerror = () => reject(tx.error) + txn.onerror = () => reject(txn.error) }) } diff --git a/src/database/idbInterface.js b/src/database/idbInterface.js index bcd177ce..037c5065 100644 --- a/src/database/idbInterface.js +++ b/src/database/idbInterface.js @@ -8,7 +8,7 @@ import { } from './constants' import { transformEmojiData } from './utils/transformEmojiData' import { extractTokens } from './utils/extractTokens' -import { getAllIDB, getIDB } from './idbUtil' +import { commit, getAllIDB, getIDB } from './idbUtil' import { findCommonMembers } from './utils/findCommonMembers' import { normalizeTokens } from './utils/normalizeTokens' @@ -38,7 +38,7 @@ async function doFullDatabaseScanForSingleResult (db, predicate) { // console.log(performance.getEntriesByName('total').slice(-1)[0].duration) // })() const BATCH_SIZE = 50 // Typically around 150ms for 6x slowdown in Chrome for above benchmark - return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => { + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { let lastKey const processNextBatch = () => { @@ -64,7 +64,7 @@ export async function loadData (db, emojiData, url, eTag) { performance.mark('loadData') try { const transformedData = transformEmojiData(emojiData) - await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore]) => { + await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => { let oldETag let oldUrl let todo = 0 @@ -88,6 +88,7 @@ export async function loadData (db, emojiData, url, eTag) { } metaStore.put(eTag, KEY_ETAG) metaStore.put(url, KEY_URL) + commit(txn) performance.mark('commitAllData') } @@ -108,7 +109,7 @@ export async function loadData (db, emojiData, url, eTag) { } export async function getEmojiByGroup (db, group) { - return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => { + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true) getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb) }) @@ -121,7 +122,7 @@ export async function getEmojiBySearchQuery (db, query) { return [] } - return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => { + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { // get all results that contain all tokens (i.e. an AND query) const intermediateResults = [] @@ -171,7 +172,7 @@ export async function getEmojiByShortcode (db, shortcode) { } export async function getEmojiByUnicode (db, unicode) { - return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => ( + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => ( getIDB(emojiStore, unicode, result => { if (result) { return cb(result) @@ -182,30 +183,32 @@ export async function getEmojiByUnicode (db, unicode) { } export function get (db, storeName, key) { - return dbPromise(db, storeName, MODE_READONLY, (store, cb) => ( + return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => ( getIDB(store, key, cb) )) } export function set (db, storeName, key, value) { - return dbPromise(db, storeName, MODE_READWRITE, (store) => ( + return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => { store.put(value, key) - )) + commit(txn) + }) } export function incrementFavoriteEmojiCount (db, unicode) { - return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store) => { - getIDB(store, unicode, result => ( + return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => ( + getIDB(store, unicode, result => { store.put((result || 0) + 1, unicode) - )) - }) + commit(txn) + }) + )) } export function getTopFavoriteEmoji (db, customEmojiIndex, limit) { if (limit === 0) { return [] } - return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], cb) => { + return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => { const results = [] favoritesStore.index(INDEX_COUNT).openCursor(undefined, 'prev').onsuccess = e => { const cursor = e.target.result diff --git a/src/database/idbUtil.js b/src/database/idbUtil.js index 2d35c075..40b77355 100644 --- a/src/database/idbUtil.js +++ b/src/database/idbUtil.js @@ -11,3 +11,10 @@ export function getIDB (store, key, cb) { export function getAllIDB (store, key, cb) { callStore(store, 'getAll', key, cb) } + +export function commit (txn) { + /* istanbul ignore else */ + if (txn.commit) { + txn.commit() + } +} diff --git a/test/benchmark/benchmark.js b/test/benchmark/benchmark.js index 35268351..88516183 100644 --- a/test/benchmark/benchmark.js +++ b/test/benchmark/benchmark.js @@ -10,7 +10,7 @@ performance.mark = function (name) { performance.measure = function (name, start) { if (name === 'initialLoad' && start === 'initialLoad') { // test to make sure the picker loaded with no errors - const hasErrors = !!document.querySelector('emoji-picker') + const hasErrors = document.querySelector('emoji-picker') && document.querySelector('emoji-picker') .shadowRoot.querySelector('.message:not(.gone)') if (hasErrors) { console.error('picker is showing an error message') diff --git a/test/benchmark/database-interactions.html b/test/benchmark/database-interactions.html new file mode 100644 index 00000000..c1760518 --- /dev/null +++ b/test/benchmark/database-interactions.html @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/benchmark/database-interactions.tachometer.json b/test/benchmark/database-interactions.tachometer.json new file mode 100644 index 00000000..60faa5b7 --- /dev/null +++ b/test/benchmark/database-interactions.tachometer.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://raw.githubusercontent.com/Polymer/tachometer/master/config.schema.json", + "sampleSize": 50, + "timeout": 5, + "horizons": ["10%"], + "benchmarks": [ + { + "url": "./database-interactions.html", + "browser": { + "name": "chrome", + "headless": true + }, + "measurement": [ + { + "mode": "performance", + "entryName": "benchmark-total" + } + ], + "expand": [ + { + "name": "this-change" + }, + { + "name": "tip-of-tree", + "packageVersions": { + "label": "tip-of-tree", + "dependencies": { + "emoji-picker-element": { + "kind": "git", + "repo": "https://github.com/nolanlawson/emoji-picker-element.git", + "ref": "master", + "setupCommands": [ + "yarn --immutable --ignore-scripts", + "PERF=1 yarn build:rollup" + ] + } + } + } + } + ] + } + ] +}