Skip to content

Commit 3f92eb6

Browse files
authored
Better handle markup rendering errors in noetebooks (microsoft#161153)
Better handle markup rendering errors Show an error to users if rendering of markup items fail. Previously when a renderer fails to load or render, we would not show any user facing errors (and would sometimes prevent the entire notebook from loading)
1 parent 2ae5bc6 commit 3f92eb6

File tree

1 file changed

+81
-48
lines changed

1 file changed

+81
-48
lines changed

src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -716,8 +716,18 @@ async function webviewPreloads(ctx: PreloadContext) {
716716
};
717717
}
718718

719-
function showPreloadErrors(outputNode: HTMLElement, ...errors: readonly Error[]) {
720-
outputNode.innerText = `Error loading preloads:`;
719+
async function renderOutputItem(rendererApi: rendererApi.RendererApi, rendererId: string, item: rendererApi.OutputItem, element: HTMLElement, signal: AbortSignal) {
720+
try {
721+
await rendererApi.renderOutputItem(item, element, signal);
722+
} catch (e) {
723+
if (!signal.aborted) {
724+
showRenderError(`Error rendering output item using '${rendererId}'`, element, e instanceof Error ? [e] : []);
725+
}
726+
}
727+
}
728+
729+
function showRenderError(errorText: string, outputNode: HTMLElement, errors: readonly Error[]) {
730+
outputNode.innerText = errorText;
721731
const errList = document.createElement('ul');
722732
for (const result of errors) {
723733
console.error(result);
@@ -1087,8 +1097,8 @@ async function webviewPreloads(ctx: PreloadContext) {
10871097

10881098
case 'html': {
10891099
const data = event.data;
1090-
outputRunner.enqueue(data.outputId, (state) => {
1091-
return viewModel.renderOutputCell(data, state);
1100+
outputRunner.enqueue(data.outputId, signal => {
1101+
return viewModel.renderOutputCell(data, signal);
10921102
});
10931103
break;
10941104
}
@@ -1336,27 +1346,32 @@ async function webviewPreloads(ctx: PreloadContext) {
13361346
};
13371347

13381348
const outputRunner = new class {
1339-
private readonly outputs = new Map<string, { cancelled: boolean; queue: Promise<unknown> }>();
1349+
private readonly outputs = new Map<string, { abort: AbortController; queue: Promise<unknown> }>();
13401350

13411351
/**
13421352
* Pushes the action onto the list of actions for the given output ID,
13431353
* ensuring that it's run in-order.
13441354
*/
1345-
public enqueue(outputId: string, action: (record: { cancelled: boolean }) => unknown) {
1355+
public enqueue(outputId: string, action: (cancelSignal: AbortSignal) => unknown) {
13461356
const record = this.outputs.get(outputId);
13471357
if (!record) {
1348-
this.outputs.set(outputId, { cancelled: false, queue: new Promise(r => r(action({ cancelled: false }))) });
1358+
const controller = new AbortController();
1359+
this.outputs.set(outputId, { abort: controller, queue: new Promise(r => r(action(controller.signal))) });
13491360
} else {
1350-
record.queue = record.queue.then(r => !record.cancelled && action(record));
1361+
record.queue = record.queue.then(r => {
1362+
if (!record.abort.signal.aborted) {
1363+
return action(record.abort.signal);
1364+
}
1365+
});
13511366
}
13521367
}
13531368

13541369
/**
13551370
* Cancels the rendering of all outputs.
13561371
*/
13571372
public cancelAll() {
1358-
for (const record of this.outputs.values()) {
1359-
record.cancelled = true;
1373+
for (const { abort } of this.outputs.values()) {
1374+
abort.abort();
13601375
}
13611376
this.outputs.clear();
13621377
}
@@ -1367,7 +1382,7 @@ async function webviewPreloads(ctx: PreloadContext) {
13671382
public cancelOutput(outputId: string) {
13681383
const output = this.outputs.get(outputId);
13691384
if (output) {
1370-
output.cancelled = true;
1385+
output.abort.abort();
13711386
this.outputs.delete(outputId);
13721387
}
13731388
}
@@ -1458,8 +1473,8 @@ async function webviewPreloads(ctx: PreloadContext) {
14581473
}
14591474

14601475
public async render(info: rendererApi.OutputItem, element: HTMLElement, signal: AbortSignal): Promise<void> {
1461-
const renderers = Array.from(this._renderers.values())
1462-
.filter(renderer => renderer.data.mimeTypes.includes(info.mime) && !renderer.data.extends);
1476+
const renderers = Array.from(this._renderers.entries())
1477+
.filter(([_, renderer]) => renderer.data.mimeTypes.includes(info.mime) && !renderer.data.extends);
14631478

14641479
if (!renderers.length) {
14651480
const errorContainer = document.createElement('div');
@@ -1482,12 +1497,23 @@ async function webviewPreloads(ctx: PreloadContext) {
14821497
}
14831498

14841499
// De-prioritize built-in renderers
1485-
renderers.sort((a, b) => +a.data.isBuiltin - +b.data.isBuiltin);
1500+
renderers.sort((a, b) => +a[1].data.isBuiltin - +b[1].data.isBuiltin);
1501+
const renderer = renderers[0];
14861502

1487-
const renderer = await renderers[0].load();
1488-
if (renderer) {
1489-
await renderer.renderOutputItem(info, element, signal);
1503+
let renderApi: rendererApi.RendererApi | undefined;
1504+
try {
1505+
renderApi = await renderer[1].load();
1506+
if (!renderApi) {
1507+
return;
1508+
}
1509+
} catch (e) {
1510+
if (!signal.aborted) {
1511+
showRenderError(`Error loading renderer '${renderer[0]}'`, element, e instanceof Error ? [e] : []);
1512+
}
1513+
return;
14901514
}
1515+
1516+
await renderOutputItem(renderApi, renderer[0], info, element, signal);
14911517
}
14921518
}();
14931519

@@ -1610,22 +1636,18 @@ async function webviewPreloads(ctx: PreloadContext) {
16101636
}
16111637
}
16121638

1613-
public async renderOutputCell(data: webviewMessages.ICreationRequestMessage, state: { cancelled: boolean }): Promise<void> {
1639+
public async renderOutputCell(data: webviewMessages.ICreationRequestMessage, signal: AbortSignal): Promise<void> {
16141640
const preloadsAndErrors = await Promise.all<unknown>([
16151641
data.rendererId ? renderers.load(data.rendererId) : undefined,
16161642
...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)),
16171643
].map(p => p?.catch(err => err)));
16181644

1619-
if (state.cancelled) {
1645+
if (signal.aborted) {
16201646
return;
16211647
}
16221648

16231649
const cellOutput = this.ensureOutputCell(data.cellId, data.cellTop, false);
1624-
const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left, data.cellId);
1625-
outputNode.render(data.content, preloadsAndErrors);
1626-
1627-
// don't hide until after this step so that the height is right
1628-
cellOutput.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible';
1650+
await cellOutput.renderOutputElement(data, data.rendererId ?? 'preload', preloadsAndErrors, signal);
16291651
}
16301652

16311653
public ensureOutputCell(cellId: string, cellTop: number, skipCellTopUpdateIfExist: boolean): OutputCell {
@@ -1927,7 +1949,6 @@ async function webviewPreloads(ctx: PreloadContext) {
19271949
}
19281950

19291951
class OutputCell {
1930-
19311952
public readonly element: HTMLElement;
19321953
private readonly outputElements = new Map</*outputId*/ string, OutputContainer>();
19331954

@@ -1957,15 +1978,23 @@ async function webviewPreloads(ctx: PreloadContext) {
19571978
this.outputElements.clear();
19581979
}
19591980

1960-
public createOutputElement(outputId: string, outputOffset: number, left: number, cellId: string): OutputElement {
1961-
let outputContainer = this.outputElements.get(outputId);
1981+
private createOutputElement(data: webviewMessages.ICreationRequestMessage): OutputElement {
1982+
let outputContainer = this.outputElements.get(data.outputId);
19621983
if (!outputContainer) {
1963-
outputContainer = new OutputContainer(outputId);
1984+
outputContainer = new OutputContainer(data.outputId);
19641985
this.element.appendChild(outputContainer.element);
1965-
this.outputElements.set(outputId, outputContainer);
1986+
this.outputElements.set(data.outputId, outputContainer);
19661987
}
19671988

1968-
return outputContainer.createOutputElement(outputId, outputOffset, left, cellId);
1989+
return outputContainer.createOutputElement(data.outputId, data.outputOffset, data.left, data.cellId);
1990+
}
1991+
1992+
public async renderOutputElement(data: webviewMessages.ICreationRequestMessage, rendererId: string, preloadsAndErrors: unknown[], signal: AbortSignal) {
1993+
const outputElement = this.createOutputElement(data);
1994+
await outputElement.render(data.content, rendererId, preloadsAndErrors, signal);
1995+
1996+
// don't hide until after this step so that the height is right
1997+
outputElement.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible';
19691998
}
19701999

19712000
public clearOutput(outputId: string, rendererId: string | undefined) {
@@ -2091,7 +2120,11 @@ async function webviewPreloads(ctx: PreloadContext) {
20912120

20922121
class OutputElement {
20932122
public readonly element: HTMLElement;
2094-
private _content?: { content: webviewMessages.ICreationContent; preloadsAndErrors: unknown[] };
2123+
private _content?: {
2124+
content: webviewMessages.ICreationContent;
2125+
rendererId: string;
2126+
preloadsAndErrors: unknown[];
2127+
};
20952128
private hasResizeObserver = false;
20962129

20972130
private renderTaskAbort?: AbortController;
@@ -2122,34 +2155,34 @@ async function webviewPreloads(ctx: PreloadContext) {
21222155
this.renderTaskAbort = undefined;
21232156
}
21242157

2125-
public async render(content: webviewMessages.ICreationContent, preloadsAndErrors: unknown[]) {
2158+
public async render(content: webviewMessages.ICreationContent, rendererId: string, preloadsAndErrors: unknown[], signal?: AbortSignal) {
21262159
this.renderTaskAbort?.abort();
21272160
this.renderTaskAbort = undefined;
21282161

2129-
this._content = { content, preloadsAndErrors };
2162+
this._content = { content, rendererId, preloadsAndErrors };
21302163
if (content.type === 0 /* RenderOutputType.Html */) {
21312164
const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent;
21322165
this.element.innerHTML = trustedHtml as string;
21332166
domEval(this.element);
21342167
} else if (preloadsAndErrors.some(e => e instanceof Error)) {
21352168
const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error);
2136-
showPreloadErrors(this.element, ...errors);
2169+
showRenderError(`Error loading preloads`, this.element, errors);
21372170
} else {
21382171
const rendererApi = preloadsAndErrors[0] as rendererApi.RendererApi;
2139-
try {
2140-
const item = createOutputItem(this.outputId, content.mimeType, content.metadata, content.valueBytes);
2172+
const item = createOutputItem(this.outputId, content.mimeType, content.metadata, content.valueBytes);
21412173

2142-
const controller = new AbortController();
2143-
this.renderTaskAbort = controller;
2144-
try {
2145-
await rendererApi.renderOutputItem(item, this.element, this.renderTaskAbort.signal);
2146-
} finally {
2147-
if (this.renderTaskAbort === controller) {
2148-
this.renderTaskAbort = undefined;
2149-
}
2174+
const controller = new AbortController();
2175+
this.renderTaskAbort = controller;
2176+
2177+
// Abort rendering if caller aborts
2178+
signal?.addEventListener('abort', () => controller.abort());
2179+
2180+
try {
2181+
await renderOutputItem(rendererApi, rendererId, item, this.element, controller.signal);
2182+
} finally {
2183+
if (this.renderTaskAbort === controller) {
2184+
this.renderTaskAbort = undefined;
21502185
}
2151-
} catch (e) {
2152-
showPreloadErrors(this.element, e);
21532186
}
21542187
}
21552188

@@ -2188,14 +2221,14 @@ async function webviewPreloads(ctx: PreloadContext) {
21882221

21892222
public rerender() {
21902223
if (this._content) {
2191-
this.render(this._content.content, this._content.preloadsAndErrors);
2224+
this.render(this._content.content, this._content.rendererId, this._content.preloadsAndErrors);
21922225
}
21932226
}
21942227

21952228
public updateAndRerender(content: webviewMessages.ICreationContent) {
21962229
if (this._content) {
21972230
this._content.content = content;
2198-
this.render(this._content.content, this._content.preloadsAndErrors);
2231+
this.render(this._content.content, this._content.rendererId, this._content.preloadsAndErrors);
21992232
}
22002233
}
22012234
}

0 commit comments

Comments
 (0)