[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:
Manav Rathi 2024-04-08 13:19:25 +05:30 committed by GitHub
commit 78f4f9b42d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 122 additions and 151 deletions

View file

@ -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();

View file

@ -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,

View file

@ -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) ||

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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";

View file

@ -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;

View 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);