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
This commit is contained in:
Nolan Lawson 2021-08-21 14:48:38 -07:00 committed by GitHub
parent e89ed42716
commit d732610e7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 123 additions and 21 deletions

View File

@ -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

View File

@ -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)
})
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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')

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script type="module" src="./benchmark.js"></script>
<script type="module">
import Database from 'emoji-picker-element/database'
performance.mark('initialLoad')
const dataSource = '/data.json'
const database = new Database({ dataSource })
await database.ready()
for (let i = 0; i < 10; i++) {
await database.getEmojiByUnicodeOrName('💥')
await database.getEmojiBySearchQuery('boom')
await database.getEmojiByShortcode('boom')
await database.getEmojiByGroup(1)
await database.getPreferredSkinTone()
await database.getTopFavoriteEmoji(10)
await database.incrementFavoriteEmojiCount('💥')
await database.setPreferredSkinTone(0)
}
performance.measure('initialLoad', 'initialLoad')
</script>
</body>
</html>

View File

@ -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"
]
}
}
}
}
]
}
]
}