Skip to content

Commit

Permalink
perf: use relaxed IDB transactions and manually commit (#218)
Browse files Browse the repository at this point in the history
* perf: use relaxed IDB transactions and manually commit

* fix: reduce time spent in benchmark

* fix: add missing txn.commit()

* fix: small syntax change
  • Loading branch information
nolanlawson committed Aug 21, 2021
1 parent e89ed42 commit d732610
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 21 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/benchmarks.yml
Expand Up @@ -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
14 changes: 8 additions & 6 deletions src/database/databaseLifecycle.js
Expand Up @@ -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)
})
}

Expand Down
31 changes: 17 additions & 14 deletions src/database/idbInterface.js
Expand Up @@ -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'

Expand Down Expand Up @@ -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 = () => {
Expand All @@ -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
Expand All @@ -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')
}

Expand All @@ -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)
})
Expand All @@ -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 = []

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/database/idbUtil.js
Expand Up @@ -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()
}
}
2 changes: 1 addition & 1 deletion test/benchmark/benchmark.js
Expand Up @@ -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')
Expand Down
31 changes: 31 additions & 0 deletions test/benchmark/database-interactions.html
@@ -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>
43 changes: 43 additions & 0 deletions 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"
]
}
}
}
}
]
}
]
}

0 comments on commit d732610

Please sign in to comment.