perf: move multipreview to app

pull/460/head
Varun Patil 2023-02-25 16:26:49 -08:00
parent 3f61f9484f
commit dd976d3c68
5 changed files with 147 additions and 66 deletions

View File

@ -44,8 +44,8 @@
@touchend.passive="$emit('touchend', $event)" @touchend.passive="$emit('touchend', $event)"
@touchcancel.passive="$emit('touchend', $event)" @touchcancel.passive="$emit('touchend', $event)"
> >
<img <XImg
ref="img" ref="ximg"
:class="['fill-block', `memories-thumb-${data.key}`]" :class="['fill-block', `memories-thumb-${data.key}`]"
draggable="false" draggable="false"
:src="src" :src="src"
@ -73,6 +73,8 @@ import { getPreviewUrl } from "../../services/FileUtils";
import { IDay, IPhoto } from "../../types"; import { IDay, IPhoto } from "../../types";
import * as utils from "../../services/Utils"; import * as utils from "../../services/Utils";
import XImg from "./XImg.vue";
import errorsvg from "../../assets/error.svg"; import errorsvg from "../../assets/error.svg";
import CheckCircle from "vue-material-design-icons/CheckCircle.vue"; import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
import Star from "vue-material-design-icons/Star.vue"; import Star from "vue-material-design-icons/Star.vue";
@ -82,6 +84,7 @@ import LivePhoto from "vue-material-design-icons/MotionPlayOutline.vue";
export default defineComponent({ export default defineComponent({
name: "Photo", name: "Photo",
components: { components: {
XImg,
CheckCircle, CheckCircle,
Video, Video,
Star, Star,
@ -205,7 +208,7 @@ export default defineComponent({
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
const img = this.$refs.img as HTMLImageElement; const img = (this.$refs.ximg as any).$el as HTMLImageElement;
canvas.width = img.naturalWidth; canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight; canvas.height = img.naturalHeight;

View File

@ -0,0 +1,66 @@
<template>
<img :alt="alt" :src="dataSrc" @load="load" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { fetchImage } from "./XImgCache";
const BLANK_IMG =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
export default defineComponent({
name: "XImg",
props: {
src: {
type: String,
required: false,
},
alt: {
type: String,
default: "",
},
},
data: () => {
return {
dataSrc: BLANK_IMG,
};
},
watch: {
src() {
this.loadImage();
},
},
mounted() {
this.loadImage();
},
methods: {
async loadImage() {
if (!this.src) return;
// Just set src if not http
if (this.src.startsWith("data:") || this.src.startsWith("blob:")) {
this.dataSrc = this.src;
return;
}
// Fetch image with axios
try {
this.dataSrc = URL.createObjectURL(await fetchImage(this.src));
} catch (error) {
this.dataSrc = BLANK_IMG;
this.$emit("error", error);
}
},
load() {
if (this.dataSrc === BLANK_IMG) return;
this.$emit("load", this.dataSrc);
},
},
});
</script>

View File

@ -1,8 +1,10 @@
import { registerRoute } from "workbox-routing";
import { CacheExpiration } from "workbox-expiration"; import { CacheExpiration } from "workbox-expiration";
import { API } from "../../services/API";
import axios from "@nextcloud/axios";
// Queue of requests to fetch preview images // Queue of requests to fetch preview images
interface FetchPreviewObject { interface FetchPreviewObject {
origUrl: string;
url: URL; url: URL;
fileid: number; fileid: number;
reqid: number; reqid: number;
@ -35,7 +37,7 @@ async function flushPreviewQueue() {
// Check if only one request // Check if only one request
if (fetchPreviewQueueCopy.length === 1) { if (fetchPreviewQueueCopy.length === 1) {
const p = fetchPreviewQueueCopy[0]; const p = fetchPreviewQueueCopy[0];
return p.callback(await fetch(p.url)); return p.callback(await fetchOneImage(p.origUrl));
} }
// Create aggregated request body // Create aggregated request body
@ -48,26 +50,15 @@ async function flushPreviewQueue() {
})); }));
try { try {
// infer the url from the first file
const firstUrl = fetchPreviewQueueCopy[0].url;
const url = new URL(firstUrl.toString());
const path = url.pathname.split("/");
const previewIndex = path.indexOf("preview");
url.pathname = path.slice(0, previewIndex).join("/") + "/multipreview";
url.searchParams.delete("x");
url.searchParams.delete("y");
url.searchParams.delete("a");
url.searchParams.delete("c");
// Fetch multipreview // Fetch multipreview
const res = await fetch(url, { const multiUrl = API.IMAGE_MULTIPREVIEW();
method: "POST", const res = await axios.post(multiUrl, files, {
body: JSON.stringify(files), responseType: "blob",
}); });
// Get blob // Get blob
if (res.status !== 200) throw new Error("Error fetching multi-preview"); if (res.status !== 200) throw new Error("Error fetching multi-preview");
const blob = await res.blob(); const blob = res.data;
let idx = 0; let idx = 0;
while (idx < blob.size) { while (idx < blob.size) {
@ -80,8 +71,6 @@ async function flushPreviewQueue() {
const reqid = jsonParsed["reqid"]; const reqid = jsonParsed["reqid"];
idx += newlineIndex + 1; idx += newlineIndex + 1;
console.debug("multi-preview", jsonParsed);
// Read the image data // Read the image data
const imgBlob = blob.slice(idx, idx + imgLen); const imgBlob = blob.slice(idx, idx + imgLen);
idx += imgLen; idx += imgLen;
@ -91,14 +80,11 @@ async function flushPreviewQueue() {
.filter((p) => p.reqid === reqid) .filter((p) => p.reqid === reqid)
.forEach((p) => { .forEach((p) => {
p.callback( p.callback(
new Response(imgBlob, { getResponse(imgBlob, imgType, {
status: 200, "Content-Type": imgType,
headers: { "Content-Length": imgLen,
"Content-Type": imgType, "Cache-Control": res.headers["Cache-Control"],
"Content-Length": imgLen, Expires: res.headers["Expires"],
Expires: res.headers.get("Expires"),
"Cache-Control": res.headers.get("Cache-Control"),
},
}) })
); );
p.callback = null; p.callback = null;
@ -119,46 +105,70 @@ async function flushPreviewQueue() {
}); });
} }
// Intercept preview requests /** Accepts a URL and returns a promise with a blob */
registerRoute( export async function fetchImage(url: string): Promise<Blob> {
/^.*\/apps\/memories\/api\/image\/preview\/.*/, // Check if in cache
async ({ url, request }) => { const cache = await imageCache?.match(url);
// Check if in cache if (cache) return await cache.blob();
const cache = await imageCache?.match(url);
if (cache) return cache;
// Get file id from URL // Get file id from URL
const fileid = Number(url.pathname.split("/").pop()); const urlObj = new URL(url, window.location.origin);
const fileid = Number(urlObj.pathname.split("/").pop());
// Aggregate requests // Check if preview image
let res: Response = await new Promise((callback) => { const regex = /^.*\/apps\/memories\/api\/image\/preview\/.*/;
// Aggregate requests
let res: Response;
if (regex.test(url)) {
res = await new Promise((callback) => {
fetchPreviewQueue.push({ fetchPreviewQueue.push({
url, origUrl: url,
url: urlObj,
fileid, fileid,
reqid: Math.random(), reqid: Math.random(),
callback, callback,
}); });
if (!fetchPreviewTimer) { if (!fetchPreviewTimer) {
fetchPreviewTimer = setTimeout(flushPreviewQueue, 50); fetchPreviewTimer = setTimeout(flushPreviewQueue, 10);
} }
}); });
// Fallback to single request
if (res.status !== 200) {
res = await fetch(url);
}
// Cache response
if (res.status === 200) {
imageCache?.put(request, res.clone());
expirationManager.updateTimestamp(request.url);
}
// Run expiration once in every 20 requests
if (Math.random() < 0.05) {
expirationManager.expireEntries();
}
return res;
} }
);
// Fallback to single request
if (!res || res.status !== 200) {
res = await fetchOneImage(url);
}
// Cache response
if (res.status === 200) {
imageCache?.put(url, res.clone());
expirationManager.updateTimestamp(url.toString());
}
// Run expiration once in every 100 requests
if (Math.random() < 0.01) {
expirationManager.expireEntries();
}
return await res.blob();
}
export async function fetchOneImage(url: string) {
const res = await axios.get(url, {
responseType: "blob",
});
return getResponse(res.data, res.headers["content-type"], res.headers);
}
function getResponse(blob: Blob, type: string, headers: any = {}) {
return new Response(blob, {
status: 200,
headers: {
"Content-Type": type,
"Content-Length": blob.size.toString(),
...headers,
},
});
}

View File

@ -5,10 +5,9 @@ import { ExpirationPlugin } from 'workbox-expiration';
precacheAndRoute(self.__WB_MANIFEST); precacheAndRoute(self.__WB_MANIFEST);
import './service-worker-custom';
registerRoute(/^.*\/apps\/memories\/api\/video\/transcode\/.*/, new NetworkOnly()); registerRoute(/^.*\/apps\/memories\/api\/video\/transcode\/.*/, new NetworkOnly());
registerRoute(/^.*\/apps\/memories\/api\/image\/jpeg\/.*/, new NetworkOnly()); registerRoute(/^.*\/apps\/memories\/api\/image\/jpeg\/.*/, new NetworkOnly());
registerRoute(/^.*\/apps\/memories\/api\/image\/preview\/.*/, new NetworkOnly());
registerRoute(/^.*\/remote.php\/.*/, new NetworkOnly()); registerRoute(/^.*\/remote.php\/.*/, new NetworkOnly());
registerRoute(/^.*\/apps\/files\/ajax\/download.php?.*/, new NetworkOnly()); registerRoute(/^.*\/apps\/files\/ajax\/download.php?.*/, new NetworkOnly());
@ -22,7 +21,6 @@ const imageCache = new CacheFirst({
], ],
}); });
registerRoute(/^.*\/apps\/memories\/api\/image\/preview\/.*/, imageCache);
registerRoute(/^.*\/apps\/memories\/api\/video\/livephoto\/.*/, imageCache); registerRoute(/^.*\/apps\/memories\/api\/video\/livephoto\/.*/, imageCache);
registerRoute(/^.*\/apps\/memories\/api\/faces\/preview\/.*/, imageCache); registerRoute(/^.*\/apps\/memories\/api\/faces\/preview\/.*/, imageCache);
registerRoute(/^.*\/apps\/memories\/api\/tags\/preview\/.*/, imageCache); registerRoute(/^.*\/apps\/memories\/api\/tags\/preview\/.*/, imageCache);

View File

@ -82,6 +82,10 @@ export class API {
return tok(gen(`${BASE}/image/preview/{fileid}`, { fileid })); return tok(gen(`${BASE}/image/preview/{fileid}`, { fileid }));
} }
static IMAGE_MULTIPREVIEW() {
return tok(gen(`${BASE}/image/multipreview`));
}
static IMAGE_INFO(id: number) { static IMAGE_INFO(id: number) {
return tok(gen(`${BASE}/image/info/{id}`, { id })); return tok(gen(`${BASE}/image/info/{id}`, { id }));
} }