Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Aug 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 47 additions & 6 deletions src/routes/_actions/createMakeProps.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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.
Expand All @@ -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)
})
}
}
9 changes: 2 additions & 7 deletions src/routes/_components/status/Media.html
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -150,6 +145,7 @@
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
Expand All @@ -166,7 +162,6 @@
},
data: () => ({
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
decodedBlurhash: ONE_TRANSPARENT_PIXEL,
mouseover: void 0
}),
store: () => store,
Expand Down
1 change: 1 addition & 0 deletions src/routes/_static/blurhash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const BLURHASH_RESOLUTION = 32
74 changes: 36 additions & 38 deletions src/routes/_utils/blurhash.js
Original file line number Diff line number Diff line change
@@ -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)
}
function initCanvas () {
if (!canvas) {
canvas = document.createElement('canvas')
canvas.height = RESOLUTION
canvas.width = RESOLUTION
canvasContext2D = canvas.getContext('2d')
}
}

if (decoded) {
resolve(decoded)
} else {
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)
const blob = await new Promise(resolve => canvas.toBlob(resolve))
return URL.createObjectURL(blob)
}

canvasContext2D.putImageData(imageData, 0, 0)
canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob))
})
}
}
async function decodeWithoutCache (blurhash) {
init()
const { decoded, imageData } = await worker.postMessage(blurhash)
if (decoded) {
return decoded
}
return decodeUsingCanvas(imageData)
}

worker.addEventListener('message', onMessage)
worker.postMessage({ encoded: blurhash })
} catch (e) {
reject(e)
}
})
export async function decode (blurhash) {
let result = CACHE.get(blurhash)
if (!result) {
result = await decodeWithoutCache(blurhash)
CACHE.set(blurhash, result)
}
return result
}
49 changes: 15 additions & 34 deletions src/routes/_workers/blurhash.js
Original file line number Diff line number Diff line change
@@ -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 {
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)
registerPromiseWorker(async (encoded) => {
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)

if (pixels) {
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
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 decoded = URL.createObjectURL(blob)
CACHE.set(encoded, decoded)
postMessage({ encoded, decoded, imageData: null, error: null })
}).catch(error => {
postMessage({ encoded, decoded: null, imageData: null, error })
})
} 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 })
if (OFFSCREEN_CANVAS) {
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
const blob = await OFFSCREEN_CANVAS.convertToBlob()
const decoded = URL.createObjectURL(blob)
return { decoded, imageData: null }
} else {
return { imageData, decoded: null }
}
})
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down