blob: b1d62fc510bf2a04d092f1d89f131d758693ca6b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import type { ProfileGraphIR } from './profileGraphTypes';
import { MAX_PARSER_BYTES, ProfileParserError, type ProfileParserErrorCode } from './profileParser';
import type { ProfileParseRequest, ProfileParseResponse } from './profileParserProtocol';
export const DEFAULT_PROFILE_PARSE_TIMEOUT_MS = 8_000;
export interface ProfileParserWorker {
onmessage: ((event: MessageEvent<ProfileParseResponse>) => void) | null;
onerror: ((event: ErrorEvent) => void) | null;
postMessage(message: ProfileParseRequest): void;
terminate(): void;
}
export type ProfileParserWorkerFactory = () => ProfileParserWorker;
export interface ProfileParseOperation {
promise: Promise<ProfileGraphIR>;
cancel(): void;
}
export function profileParserErrorMessage(code: ProfileParserErrorCode): string {
if (code === 'DAG_TOO_LARGE') return 'This execution graph is too large to display.';
if (code === 'DAG_UNAVAILABLE') return 'An execution graph is not available for this Profile.';
return 'The execution graph could not be generated.';
}
function requestId(): string {
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
const bytes = crypto.getRandomValues(new Uint8Array(16));
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
}
export function startProfileParse(
text: string,
createWorker: ProfileParserWorkerFactory,
timeoutMs = DEFAULT_PROFILE_PARSE_TIMEOUT_MS,
): ProfileParseOperation {
const id = requestId();
let worker: ProfileParserWorker | null = null;
let settled = false;
let rejectPromise: ((reason: unknown) => void) | null = null;
let timeout: ReturnType<typeof setTimeout> | null = null;
const finish = () => {
if (timeout !== null) clearTimeout(timeout);
timeout = null;
worker?.terminate();
worker = null;
};
const promise = new Promise<ProfileGraphIR>((resolve, reject) => {
rejectPromise = reject;
if (new TextEncoder().encode(text).byteLength > MAX_PARSER_BYTES) {
settled = true;
reject(new ProfileParserError('DAG_TOO_LARGE', profileParserErrorMessage('DAG_TOO_LARGE')));
return;
}
try {
worker = createWorker();
} catch {
settled = true;
reject(new ProfileParserError('DAG_PARSE_FAILED', profileParserErrorMessage('DAG_PARSE_FAILED')));
return;
}
worker.onmessage = (event) => {
if (settled || event.data.requestId !== id) return;
settled = true;
finish();
if (event.data.type === 'PARSE_SUCCESS') resolve(event.data.graph);
else reject(new ProfileParserError(event.data.code, event.data.message || profileParserErrorMessage(event.data.code)));
};
worker.onerror = () => {
if (settled) return;
settled = true;
finish();
reject(new ProfileParserError('DAG_PARSE_FAILED', profileParserErrorMessage('DAG_PARSE_FAILED')));
};
timeout = setTimeout(() => {
if (settled) return;
settled = true;
finish();
reject(new ProfileParserError('DAG_PARSE_FAILED', 'The execution graph parser timed out.'));
}, timeoutMs);
worker.postMessage({ type: 'PARSE_PROFILE', requestId: id, text });
});
return {
promise,
cancel() {
if (settled) return;
settled = true;
finish();
rejectPromise?.(new DOMException('The operation was aborted.', 'AbortError'));
},
};
}