fix: move blurhash worker operations to before status rendering (#1391)

* fix: move blurhash worker operations to before status rendering

* slight refactor

* avoid sending encoded data back and forth

* move cache outside worker
This commit is contained in:
Nolan Lawson 2019-08-17 14:36:13 -07:00 committed by GitHub
parent daa1978945
commit f8180e813f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 109 additions and 87 deletions

View file

@ -83,6 +83,7 @@
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"pinch-zoom-element": "^1.1.1", "pinch-zoom-element": "^1.1.1",
"preact": "^10.0.0-beta.1", "preact": "^10.0.0-beta.1",
"promise-worker": "^2.0.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"quick-lru": "^4.0.1", "quick-lru": "^4.0.1",
"remount": "^0.11.0", "remount": "^0.11.0",

View file

@ -1,4 +1,6 @@
import { database } from '../_database/database' import { database } from '../_database/database'
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash'
import { mark, stop } from '../_utils/marks'
async function getNotification (instanceName, timelineType, timelineValue, itemId) { async function getNotification (instanceName, timelineType, timelineValue, itemId) {
return { return {
@ -16,10 +18,38 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
} }
} }
function tryInitBlurhash () {
try {
initBlurhash()
} catch (err) {
console.error('could not start blurhash worker', err)
}
}
async function decodeAllBlurhashes (statusOrNotification) {
const status = statusOrNotification.status || statusOrNotification.notification.status
if (status && status.media_attachments) {
mark(`decodeBlurhash-${status.id}`)
await Promise.all(status.media_attachments.map(async media => {
if (media.blurhash) {
try {
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
} catch (err) {
console.warn('Could not decode blurhash, ignoring', err)
}
}
}))
stop(`decodeBlurhash-${status.id}`)
}
return statusOrNotification
}
export function createMakeProps (instanceName, timelineType, timelineValue) { export function createMakeProps (instanceName, timelineType, timelineValue) {
let taskCount = 0 let taskCount = 0
let pending = [] let pending = []
tryInitBlurhash() // start the blurhash worker a bit early to save time
// The worker-powered indexeddb promises can resolve in arbitrary order, // The worker-powered indexeddb promises can resolve in arbitrary order,
// causing the timeline to load in a jerky way. With this function, we // causing the timeline to load in a jerky way. With this function, we
// wait for all promises to resolve before resolving them all in one go. // wait for all promises to resolve before resolving them all in one go.
@ -34,14 +64,25 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
}) })
} }
async function fetchFromIndexedDB (itemId) {
mark(`fetchFromIndexedDB-${itemId}`)
try {
const res = await (timelineType === 'notifications'
? getNotification(instanceName, timelineType, timelineValue, itemId)
: getStatus(instanceName, timelineType, timelineValue, itemId))
return res
} finally {
stop(`fetchFromIndexedDB-${itemId}`)
}
}
return (itemId) => { return (itemId) => {
taskCount++ taskCount++
const promise = timelineType === 'notifications'
? getNotification(instanceName, timelineType, timelineValue, itemId)
: getStatus(instanceName, timelineType, timelineValue, itemId)
return promise.then(res => { return fetchFromIndexedDB(itemId)
return awaitAllTasksComplete().then(() => res) .then(decodeAllBlurhashes)
.then(statusOrNotification => {
return awaitAllTasksComplete().then(() => statusOrNotification)
}) })
} }
} }

View file

@ -110,16 +110,11 @@
import LazyImage from '../LazyImage.html' import LazyImage from '../LazyImage.html'
import AutoplayVideo from '../AutoplayVideo.html' import AutoplayVideo from '../AutoplayVideo.html'
import { registerClickDelegate } from '../../_utils/delegate' import { registerClickDelegate } from '../../_utils/delegate'
import { decode } from '../../_utils/blurhash'
export default { export default {
async oncreate () { async oncreate () {
const { elementId, media } = this.get() const { elementId } = this.get()
registerClickDelegate(this, elementId, () => this.onClick()) registerClickDelegate(this, elementId, () => this.onClick())
if (media.blurhash) {
this.set({ decodedBlurhash: await decode(media.blurhash) })
}
}, },
computed: { computed: {
focus: ({ meta }) => meta && meta.focus, focus: ({ meta }) => meta && meta.focus,
@ -150,6 +145,7 @@ export default {
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`, elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
description: ({ media }) => media.description || '', description: ({ media }) => media.description || '',
previewUrl: ({ media }) => media.preview_url, previewUrl: ({ media }) => media.preview_url,
decodedBlurhash: ({ media }) => media.decodedBlurhash || ONE_TRANSPARENT_PIXEL,
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash, blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash,
url: ({ media }) => media.url, url: ({ media }) => media.url,
type: ({ media }) => media.type type: ({ media }) => media.type
@ -166,7 +162,6 @@ export default {
}, },
data: () => ({ data: () => ({
oneTransparentPixel: ONE_TRANSPARENT_PIXEL, oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
decodedBlurhash: ONE_TRANSPARENT_PIXEL,
mouseover: void 0 mouseover: void 0
}), }),
store: () => store, store: () => store,

View file

@ -0,0 +1 @@
export const BLURHASH_RESOLUTION = 32

View file

@ -1,51 +1,49 @@
import BlurhashWorker from 'worker-loader!../_workers/blurhash' // eslint-disable-line import BlurhashWorker from 'worker-loader!../_workers/blurhash' // eslint-disable-line
import PromiseWorker from 'promise-worker'
import { BLURHASH_RESOLUTION as RESOLUTION } from '../_static/blurhash'
import QuickLRU from 'quick-lru'
const CACHE = new QuickLRU({ maxSize: 100 })
const RESOLUTION = 32
let worker let worker
let canvas let canvas
let canvasContext2D let canvasContext2D
export function init () { export function init () {
worker = worker || new BlurhashWorker() worker = worker || new PromiseWorker(new BlurhashWorker())
} }
export async function decode (blurhash) { function initCanvas () {
return new Promise((resolve, reject) => {
try {
init()
const onMessage = ({ data: { encoded, decoded, imageData, error } }) => {
if (encoded !== blurhash) {
return
}
worker.removeEventListener('message', onMessage)
if (error) {
return reject(error)
}
if (decoded) {
resolve(decoded)
} else {
if (!canvas) { if (!canvas) {
canvas = document.createElement('canvas') canvas = document.createElement('canvas')
canvas.height = RESOLUTION canvas.height = RESOLUTION
canvas.width = RESOLUTION canvas.width = RESOLUTION
canvasContext2D = canvas.getContext('2d') canvasContext2D = canvas.getContext('2d')
} }
}
canvasContext2D.putImageData(imageData, 0, 0)
canvas.toBlob(blob => { // canvas is the backup if we can't use OffscreenCanvas
resolve(URL.createObjectURL(blob)) async function decodeUsingCanvas (imageData) {
}) initCanvas()
} canvasContext2D.putImageData(imageData, 0, 0)
} const blob = await new Promise(resolve => canvas.toBlob(resolve))
return URL.createObjectURL(blob)
worker.addEventListener('message', onMessage) }
worker.postMessage({ encoded: blurhash })
} catch (e) { async function decodeWithoutCache (blurhash) {
reject(e) init()
} const { decoded, imageData } = await worker.postMessage(blurhash)
}) if (decoded) {
return decoded
}
return decodeUsingCanvas(imageData)
}
export async function decode (blurhash) {
let result = CACHE.get(blurhash)
if (!result) {
result = await decodeWithoutCache(blurhash)
CACHE.set(blurhash, result)
}
return result
} }

View file

@ -1,45 +1,26 @@
import { decode as decodeBlurHash } from 'blurhash' import { decode as decodeBlurHash } from 'blurhash'
import QuickLRU from 'quick-lru' import registerPromiseWorker from 'promise-worker/register'
import { BLURHASH_RESOLUTION as RESOLUTION } from '../_static/blurhash'
const RESOLUTION = 32
const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function' const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function'
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null ? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
? OFFSCREEN_CANVAS.getContext('2d') : null ? OFFSCREEN_CANVAS.getContext('2d') : null
const CACHE = new QuickLRU({ maxSize: 100 })
self.addEventListener('message', ({ data: { encoded } }) => { registerPromiseWorker(async (encoded) => {
try {
if (CACHE.has(encoded)) {
if (OFFSCREEN_CANVAS) {
postMessage({ encoded, decoded: CACHE.get(encoded), imageData: null, error: null })
} else {
postMessage({ encoded, imageData: CACHE.get(encoded), decoded: null, error: null })
}
} else {
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION) const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)
if (pixels) { if (!pixels) {
throw new Error('decode did not return any pixels')
}
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION) const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
if (OFFSCREEN_CANVAS) { if (OFFSCREEN_CANVAS) {
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0) OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
OFFSCREEN_CANVAS.convertToBlob().then(blob => { const blob = await OFFSCREEN_CANVAS.convertToBlob()
const decoded = URL.createObjectURL(blob) const decoded = URL.createObjectURL(blob)
CACHE.set(encoded, decoded) return { decoded, imageData: null }
postMessage({ encoded, decoded, imageData: null, error: null })
}).catch(error => {
postMessage({ encoded, decoded: null, imageData: null, error })
})
} else { } else {
CACHE.set(encoded, imageData) return { imageData, decoded: null }
postMessage({ encoded, imageData, decoded: null, error: null })
}
} else {
postMessage({ encoded, decoded: null, imageData: null, error: new Error('decode did not return any pixels') })
}
}
} catch (error) {
postMessage({ encoded, decoded: null, imageData: null, error })
} }
}) })

View file

@ -5991,6 +5991,11 @@ promise-polyfill@^6.0.1:
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc= integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=
promise-worker@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-worker/-/promise-worker-2.0.1.tgz#63bb532624ecd40cdb335b51bb7830c3c892aa6c"
integrity sha512-jR7vHqMEwWJ15i9vA3qyCKwRHihyLJp1sAa3RyY5F35m3u5s2lQUfq0nzVjbA8Xc7+3mL3Y9+9MHBO9UFRpFxA==
promisify-event@^1.0.0: promisify-event@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/promisify-event/-/promisify-event-1.0.0.tgz#bd7523ea06b70162f370979016b53a686c60e90f" resolved "https://registry.yarnpkg.com/promisify-event/-/promisify-event-1.0.0.tgz#bd7523ea06b70162f370979016b53a686c60e90f"