[web] Capture logs from web workers (#1365)
This started of as a refactoring of our logging layer, but turned into a bug fix (+ refactorings) when I noticed that the logs in the web/worker case were not being saved to the on disk file. Refs: - https://github.com/GoogleChromeLabs/comlink/issues/506 - https://github.com/GoogleChromeLabs/comlink/issues/568
This commit is contained in:
commit
78f4f9b42d
9 changed files with 122 additions and 151 deletions
|
@ -1,72 +0,0 @@
|
|||
import ElectronAPIs from "@ente/shared/electron";
|
||||
import { WorkerSafeElectronService } from "@ente/shared/electron/service";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { addLogLine } from "@ente/shared/logging";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
|
||||
import { ElectronFile } from "types/upload";
|
||||
|
||||
class ElectronImageProcessorService {
|
||||
async convertToJPEG(fileBlob: Blob, filename: string): Promise<Blob> {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
|
||||
const convertedFileData =
|
||||
await WorkerSafeElectronService.convertToJPEG(
|
||||
inputFileData,
|
||||
filename,
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
fileBlob?.size,
|
||||
)},convertedFileSize:${convertBytesToHumanReadable(
|
||||
convertedFileData?.length,
|
||||
)}, native conversion time: ${Date.now() - startTime}ms `,
|
||||
);
|
||||
return new Blob([convertedFileData]);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message !==
|
||||
CustomError.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
|
||||
) {
|
||||
logError(e, "failed to convert to jpeg natively");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async generateImageThumbnail(
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const thumb = await ElectronAPIs.generateImageThumbnail(
|
||||
inputFile,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
inputFile?.size,
|
||||
)},thumbFileSize:${convertBytesToHumanReadable(
|
||||
thumb?.length,
|
||||
)}, native thumbnail generation time: ${
|
||||
Date.now() - startTime
|
||||
}ms `,
|
||||
);
|
||||
return thumb;
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message !==
|
||||
CustomError.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
|
||||
) {
|
||||
logError(e, "failed to generate image thumbnail natively");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ElectronImageProcessorService();
|
|
@ -1,3 +1,4 @@
|
|||
import ElectronAPIs from "@ente/shared/electron";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { addLogLine } from "@ente/shared/logging";
|
||||
import { getFileNameSize } from "@ente/shared/logging/web";
|
||||
|
@ -8,7 +9,6 @@ import { BLACK_THUMBNAIL_BASE64 } from "constants/upload";
|
|||
import isElectron from "is-electron";
|
||||
import * as FFmpegService from "services/ffmpeg/ffmpegService";
|
||||
import HeicConversionService from "services/heicConversionService";
|
||||
import imageProcessor from "services/imageProcessor";
|
||||
import { ElectronFile, FileTypeInfo } from "types/upload";
|
||||
import { isFileHEIC } from "utils/file";
|
||||
import { getUint8ArrayView } from "../readerService";
|
||||
|
@ -86,7 +86,7 @@ async function generateImageThumbnail(
|
|||
) {
|
||||
if (isElectron()) {
|
||||
try {
|
||||
return await imageProcessor.generateImageThumbnail(
|
||||
return await generateImageThumbnailInElectron(
|
||||
file,
|
||||
MAX_THUMBNAIL_DIMENSION,
|
||||
MAX_THUMBNAIL_SIZE,
|
||||
|
@ -99,6 +99,39 @@ async function generateImageThumbnail(
|
|||
}
|
||||
}
|
||||
|
||||
const generateImageThumbnailInElectron = async (
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> => {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const thumb = await ElectronAPIs.generateImageThumbnail(
|
||||
inputFile,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
inputFile?.size,
|
||||
)},thumbFileSize:${convertBytesToHumanReadable(
|
||||
thumb?.length,
|
||||
)}, native thumbnail generation time: ${
|
||||
Date.now() - startTime
|
||||
}ms `,
|
||||
);
|
||||
return thumb;
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message !==
|
||||
CustomError.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
|
||||
) {
|
||||
logError(e, "failed to generate image thumbnail natively");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export async function generateImageThumbnailUsingCanvas(
|
||||
file: File | ElectronFile,
|
||||
fileTypeInfo: FileTypeInfo,
|
||||
|
|
|
@ -53,8 +53,8 @@ import {
|
|||
import { FileTypeInfo } from "types/upload";
|
||||
|
||||
import { default as ElectronAPIs } from "@ente/shared/electron";
|
||||
import { workerBridge } from "@ente/shared/worker/worker-bridge";
|
||||
import { t } from "i18next";
|
||||
import imageProcessor from "services/imageProcessor";
|
||||
import { getFileExportPath, getUniqueFileExportName } from "utils/export";
|
||||
|
||||
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
|
||||
|
@ -452,7 +452,7 @@ export async function getRenderableImage(fileName: string, imageBlob: Blob) {
|
|||
imageBlob.size,
|
||||
)}`,
|
||||
);
|
||||
convertedImageBlob = await imageProcessor.convertToJPEG(
|
||||
convertedImageBlob = await convertToJPEGInElectron(
|
||||
imageBlob,
|
||||
fileName,
|
||||
);
|
||||
|
@ -484,6 +484,36 @@ export async function getRenderableImage(fileName: string, imageBlob: Blob) {
|
|||
}
|
||||
}
|
||||
|
||||
const convertToJPEGInElectron = async (
|
||||
fileBlob: Blob,
|
||||
filename: string,
|
||||
): Promise<Blob> => {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
|
||||
const convertedFileData = await workerBridge.convertToJPEG(
|
||||
inputFileData,
|
||||
filename,
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
fileBlob?.size,
|
||||
)},convertedFileSize:${convertBytesToHumanReadable(
|
||||
convertedFileData?.length,
|
||||
)}, native conversion time: ${Date.now() - startTime}ms `,
|
||||
);
|
||||
return new Blob([convertedFileData]);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message !==
|
||||
CustomError.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
|
||||
) {
|
||||
logError(e, "failed to convert to jpeg natively");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export function isFileHEIC(exactType: string) {
|
||||
return (
|
||||
exactType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import { inWorker } from "@/next/env";
|
||||
import * as Comlink from "comlink";
|
||||
import { wrap } from "comlink";
|
||||
import { ElectronAPIsType } from "./types";
|
||||
import { WorkerSafeElectronClient } from "./worker/client";
|
||||
|
||||
export interface LimitedElectronAPIs
|
||||
extends Pick<ElectronAPIsType, "convertToJPEG" | "logToDisk"> {}
|
||||
|
||||
class WorkerSafeElectronServiceImpl implements LimitedElectronAPIs {
|
||||
proxiedElectron:
|
||||
| Comlink.Remote<WorkerSafeElectronClient>
|
||||
| WorkerSafeElectronClient;
|
||||
ready: Promise<any>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
private async init() {
|
||||
if (inWorker()) {
|
||||
const workerSafeElectronClient =
|
||||
wrap<typeof WorkerSafeElectronClient>(self);
|
||||
|
||||
this.proxiedElectron = await new workerSafeElectronClient();
|
||||
} else {
|
||||
this.proxiedElectron = new WorkerSafeElectronClient();
|
||||
}
|
||||
}
|
||||
|
||||
async convertToJPEG(
|
||||
inputFileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
await this.ready;
|
||||
return this.proxiedElectron.convertToJPEG(inputFileData, filename);
|
||||
}
|
||||
|
||||
async logToDisk(message: string) {
|
||||
await this.ready;
|
||||
return this.proxiedElectron.logToDisk(message);
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkerSafeElectronService = new WorkerSafeElectronServiceImpl();
|
|
@ -1,21 +0,0 @@
|
|||
import ElectronAPIs from "@ente/shared/electron";
|
||||
|
||||
export interface ProxiedLimitedElectronAPIs {
|
||||
convertToJPEG: (
|
||||
inputFileData: Uint8Array,
|
||||
filename: string,
|
||||
) => Promise<Uint8Array>;
|
||||
logToDisk: (message: string) => void;
|
||||
}
|
||||
|
||||
export class WorkerSafeElectronClient implements ProxiedLimitedElectronAPIs {
|
||||
async convertToJPEG(
|
||||
inputFileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
return await ElectronAPIs.convertToJPEG(inputFileData, filename);
|
||||
}
|
||||
logToDisk(message: string) {
|
||||
return ElectronAPIs.logToDisk(message);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,21 @@
|
|||
import { isDevBuild } from "@/next/env";
|
||||
import { inWorker, isDevBuild } from "@/next/env";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import isElectron from "is-electron";
|
||||
import { WorkerSafeElectronService } from "../electron/service";
|
||||
import ElectronAPIs from "../electron";
|
||||
import { workerBridge } from "../worker/worker-bridge";
|
||||
import { formatLog, logWeb } from "./web";
|
||||
|
||||
export const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
export const MAX_LOG_LINES = 1000;
|
||||
|
||||
export const logToDisk = (message: string) => {
|
||||
if (isElectron()) {
|
||||
ElectronAPIs.logToDisk(message);
|
||||
} else {
|
||||
logWeb(message);
|
||||
}
|
||||
};
|
||||
|
||||
export function addLogLine(
|
||||
log: string | number | boolean,
|
||||
...optionalParams: (string | number | boolean)[]
|
||||
|
@ -16,10 +25,19 @@ export function addLogLine(
|
|||
if (isDevBuild) {
|
||||
console.log(completeLog);
|
||||
}
|
||||
if (isElectron()) {
|
||||
WorkerSafeElectronService.logToDisk(completeLog);
|
||||
if (inWorker()) {
|
||||
workerBridge
|
||||
.logToDisk(completeLog)
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"Failed to log a message from worker",
|
||||
e,
|
||||
"\nThe message was",
|
||||
completeLog,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
logWeb(completeLog);
|
||||
logToDisk(completeLog);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, "failed to addLogLine", undefined, true);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { isDevBuild } from "@/next/env";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import {
|
||||
getData,
|
||||
LS_KEYS,
|
||||
getData,
|
||||
removeData,
|
||||
setData,
|
||||
} from "@ente/shared/storage/localStorage";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { WorkerSafeElectronClient } from "@ente/shared/electron/worker/client";
|
||||
import { addLocalLog } from "@ente/shared/logging";
|
||||
import { expose, Remote, wrap } from "comlink";
|
||||
import { addLocalLog, logToDisk } from "@ente/shared/logging";
|
||||
import { Remote, expose, wrap } from "comlink";
|
||||
import ElectronAPIs from "../electron";
|
||||
import { logError } from "../sentry";
|
||||
|
||||
export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
||||
|
@ -21,7 +21,7 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
|||
addLocalLog(() => `Initiated ${this.name}`);
|
||||
const comlink = wrap<T>(this.worker);
|
||||
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
|
||||
expose(WorkerSafeElectronClient, this.worker);
|
||||
expose(workerBridge, worker);
|
||||
}
|
||||
|
||||
public getName() {
|
||||
|
@ -33,3 +33,18 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
|||
addLocalLog(() => `Terminated ${this.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal set of utility functions that we expose to all workers that we
|
||||
* create.
|
||||
*
|
||||
* Inside the worker's code, this can be accessed by using the sibling
|
||||
* `workerBridge` object by importing `worker-bridge.ts`.
|
||||
*/
|
||||
const workerBridge = {
|
||||
logToDisk,
|
||||
convertToJPEG: (inputFileData: Uint8Array, filename: string) =>
|
||||
ElectronAPIs.convertToJPEG(inputFileData, filename),
|
||||
};
|
||||
|
||||
export type WorkerBridge = typeof workerBridge;
|
||||
|
|
12
web/packages/shared/worker/worker-bridge.ts
Normal file
12
web/packages/shared/worker/worker-bridge.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { wrap } from "comlink";
|
||||
import type { WorkerBridge } from "./comlinkWorker";
|
||||
|
||||
/**
|
||||
* The web worker side handle to the {@link WorkerBridge} exposed by the main
|
||||
* thread.
|
||||
*
|
||||
* This file is meant to be run inside a worker. Accessing the properties of
|
||||
* this object will be transparently (but asynchrorously) relayed to the
|
||||
* implementation of the {@link WorkerBridge} in `comlinkWorker.ts`.
|
||||
*/
|
||||
export const workerBridge = wrap<WorkerBridge>(globalThis);
|
Loading…
Reference in a new issue