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",
"pinch-zoom-element": "^1.1.1",
"preact": "^10.0.0-beta.1",
"promise-worker": "^2.0.1",
"prop-types": "^15.7.2",
"quick-lru": "^4.0.1",
"remount": "^0.11.0",

View file

@ -1,4 +1,6 @@
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) {
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) {
let taskCount = 0
let pending = []
tryInitBlurhash() // start the blurhash worker a bit early to save time
// The worker-powered indexeddb promises can resolve in arbitrary order,
// 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.
@ -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) => {
taskCount++
const promise = timelineType === 'notifications'
? getNotification(instanceName, timelineType, timelineValue, itemId)
: getStatus(instanceName, timelineType, timelineValue, itemId)
return promise.then(res => {
return awaitAllTasksComplete().then(() => res)
return fetchFromIndexedDB(itemId)
.then(decodeAllBlurhashes)
.then(statusOrNotification => {
return awaitAllTasksComplete().then(() => statusOrNotification)
})
}
}

View file

@ -110,16 +110,11 @@
import LazyImage from '../LazyImage.html'
import AutoplayVideo from '../AutoplayVideo.html'
import { registerClickDelegate } from '../../_utils/delegate'
import { decode } from '../../_utils/blurhash'
export default {
async oncreate () {
const { elementId, media } = this.get()
const { elementId } = this.get()
registerClickDelegate(this, elementId, () => this.onClick())
if (media.blurhash) {
this.set({ decodedBlurhash: await decode(media.blurhash) })
}
},
computed: {
focus: ({ meta }) => meta && meta.focus,
@ -150,6 +145,7 @@ export default {
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
description: ({ media }) => media.description || '',
previewUrl: ({ media }) => media.preview_url,
decodedBlurhash: ({ media }) => media.decodedBlurhash || ONE_TRANSPARENT_PIXEL,
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash,
url: ({ media }) => media.url,
type: ({ media }) => media.type
@ -166,7 +162,6 @@ export default {
},
data: () => ({
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
decodedBlurhash: ONE_TRANSPARENT_PIXEL,
mouseover: void 0
}),
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 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 canvas
let canvasContext2D
export function init () {
worker = worker || new BlurhashWorker()
worker = worker || new PromiseWorker(new BlurhashWorker())
}
export async function decode (blurhash) {
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 {
function initCanvas () {
if (!canvas) {
canvas = document.createElement('canvas')
canvas.height = RESOLUTION
canvas.width = RESOLUTION
canvasContext2D = canvas.getContext('2d')
}
}
// canvas is the backup if we can't use OffscreenCanvas
async function decodeUsingCanvas (imageData) {
initCanvas()
canvasContext2D.putImageData(imageData, 0, 0)
canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob))
})
}
const blob = await new Promise(resolve => canvas.toBlob(resolve))
return URL.createObjectURL(blob)
}
worker.addEventListener('message', onMessage)
worker.postMessage({ encoded: blurhash })
} catch (e) {
reject(e)
async function decodeWithoutCache (blurhash) {
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 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'
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
? OFFSCREEN_CANVAS.getContext('2d') : null
const CACHE = new QuickLRU({ maxSize: 100 })
self.addEventListener('message', ({ data: { 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 {
registerPromiseWorker(async (encoded) => {
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)
if (OFFSCREEN_CANVAS) {
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
OFFSCREEN_CANVAS.convertToBlob().then(blob => {
const blob = await OFFSCREEN_CANVAS.convertToBlob()
const decoded = URL.createObjectURL(blob)
CACHE.set(encoded, decoded)
postMessage({ encoded, decoded, imageData: null, error: null })
}).catch(error => {
postMessage({ encoded, decoded: null, imageData: null, error })
})
return { decoded, imageData: null }
} else {
CACHE.set(encoded, imageData)
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 })
return { imageData, decoded: null }
}
})

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"
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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/promisify-event/-/promisify-event-1.0.0.tgz#bd7523ea06b70162f370979016b53a686c60e90f"