scattergl remains blank after WebGL context restoration; regl refresh retains stale ANGLE_instanced_arrays
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 45/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Active
- Stack technique
- javascript
- Domaine
- computer-graphics, data-visualization
Piste de recherche
The issue is in the bundled @plotly/regl library, specifically in the generated refresh procedure that retains a stale ANGLE_instanced_arrays extension object. Start by examining lib/core.js around line 3658 in the regl repository. The reproduction HTML provides a test case; run it to see the blank scattergl after context restoration. Investigate other WebGL state restoration issues beyond the extension reference, as fixing the divisor alone did not restore rendering.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Summary
After a WebGL context is lost and restored, scattergl line traces can remain blank even after Plotly.redraw. Axes and legends remain visible, the trace data is intact, and gl.isContextLost() returns false.
This reproduces with the official Plotly.js 4.1.1 bundle in a standalone HTML page, independently of Jupyter. Investigation identified one concrete defect in the bundled @plotly/regl: its generated refresh procedure retains a pre-loss ANGLE_instanced_arrays extension object. Correcting that reference repairs attribute divisors but does not fully repair rendering, so this report does not claim that it is the only recovery defect.
Environment
- Plotly.js 4.1.1, official CDN bundle (latest stable checked on 2026-09-23).
- macOS; Chromium 153.0.0.0, in the Codex embedded browser; WebGL 1.
- Initially observed in JupyterLab with Plotly.py 7.1.0. Both the Jupyter renderer bundle and
window.Plotlywere 4.1.1. - The standalone reproduction below has no Python/Jupyter dependency. Other browsers/GPUs have not been tested.
Reproduction
- Save the following as an HTML file and open it in the browser. It uses only synthetic data.
- Confirm that four line traces are visible.
- Click Lose and restore the plot context once. This uses
WEBGL_lose_contexton this test plot only, waits for both context events, and then callsPlotly.redraw.
The internal line2d.gl path and the first six attribute locations are diagnostics for this tested version/configuration, not a proposed public API. The visual failure does not depend on inspecting those locations.
Complete standalone reproduction
<!doctype html>
<meta charset="utf-8">
<title>Plotly scattergl context restoration</title>
<script src="https://cdn.plot.ly/plotly-4.1.1.js"></script>
<button id="run" disabled>Lose and restore the plot context</button>
<div id="plot"></div>
<pre id="log"></pre>
<script>
const gd = document.getElementById('plot');
const button = document.getElementById('run');
const log = document.getElementById('log');
const data = Array.from({length: 4}, (_, j) => ({
type: 'scattergl', mode: 'lines', name: 'series ' + j,
x: Array.from({length: 403}, (_, i) =>
new Date(Date.UTC(2025, 0, 2 + i)).toISOString()),
y: Array.from({length: 403}, (_, i) =>
j === 3 ? 0 : 10000 * (Math.sin(i / 23 + j) + i * (j + 1) / 150))
}));
const once = (target, name) => new Promise(resolve =>
target.addEventListener(name, resolve, {once: true}));
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
function snapshot(gl) {
const ext = gl.getExtension('ANGLE_instanced_arrays');
const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4);
gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight,
gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let nontransparentPixels = 0;
for (let i = 3; i < pixels.length; i += 4) if (pixels[i]) nontransparentPixels++;
return {
contextLost: gl.isContextLost(),
divisors: Array.from({length: 6}, (_, i) =>
gl.getVertexAttrib(i, ext.VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE)),
nontransparentPixels
};
}
Plotly.newPlot(gd, data, {width: 642, height: 360}).then(() => {
// Internal diagnostic path for Plotly.js 4.1.1; not a public API.
const gl = gd._fullLayout._plots.xy._scene.line2d.gl;
log.textContent = JSON.stringify({version: Plotly.version, before: snapshot(gl)}, null, 2);
button.disabled = false;
button.onclick = async () => {
button.disabled = true;
try {
const before = snapshot(gl);
const oldExtension = gl.getExtension('ANGLE_instanced_arrays');
const loss = gl.getExtension('WEBGL_lose_context');
gl.canvas.addEventListener('webglcontextlost', event => event.preventDefault(), {once: true});
const lost = once(gl.canvas, 'webglcontextlost');
loss.loseContext();
await lost;
await delay(150);
const restored = once(gl.canvas, 'webglcontextrestored');
loss.restoreContext();
await restored;
await delay(150);
await Plotly.redraw(gd);
log.textContent = JSON.stringify({
version: Plotly.version, before,
sameExtensionObject: oldExtension === gl.getExtension('ANGLE_instanced_arrays'),
afterRestoreAndRedraw: snapshot(gl)
}, null, 2);
} catch (error) { log.textContent += '\n' + error.stack; }
};
});
</script>
Expected
The four line traces render again once the context is restored and the plot is redrawn.
Actual
The plotting area remains blank while the axes and legend remain. The exact reproduction above produced:
{
"version": "4.1.1",
"before": {
"contextLost": false,
"divisors": [1, 1, 1, 1, 1, 1],
"nontransparentPixels": 18394
},
"sameExtensionObject": false,
"afterRestoreAndRedraw": {
"contextLost": false,
"divisors": [0, 0, 0, 0, 0, 0],
"nontransparentPixels": 0
}
}
Pixel counts are environment-dependent; the relevant observation is the visible-to-blank transition after restoration.
Investigation
Evidence from the original failure
- The browser had already logged
WARNING: Too many active WebGL contexts. Oldest context will be lost.before investigation began. - The affected figure still had four enabled
scattergltraces, each with 403 finite y values, and valid axis ranges. Its canvas contained only a tiny fragment of a line. - Its WebGL context was no longer lost. The six line-coordinate/color attribute divisors were 0 in actual GL state, while the corresponding regl attribute cache entries were 1. Healthy neighboring line plots had matching actual/cached divisors of 1.
- Debugger inspection showed that the generated refresh closure retained an extension object different from the current
gl.getExtension('ANGLE_instanced_arrays'). The regl instance and canvas identities were correct.
The context-limit warnings explain why recovery becomes relevant. The exact allocation sequence that exceeded the limit was not recorded. The original failing output was inspected without rerunning its cell or redrawing it; subsequent experiments used a separate synthetic page.
Confirmed stale extension reference
In the published @plotly/regl 2.1.2 source, the generated refresh procedure captures the extension instance using INSTANCING = env.link(extInstancing). Context restoration calls extensionState.restore(), but the compiled refresh procedure still refers to the old object.
In a separately instrumented synthetic reproduction:
- The ANGLE extension object identity changed after restoration.
- Refresh called
vertexAttribDivisorANGLE(index, 1)through the old extension; immediately querying that attribute still returned 0. - The draw procedure used the current extension, but its cached attribute state already said divisor=1, so it skipped the assignment needed to restore actual GL state.
Partial diagnostic patch, not a complete fix
In a temporary copy of the official bundle, replacing the captured reference with a dynamic lookup:
- INSTANCING = env.link(extInstancing);
+ INSTANCING = refresh.def(shared.extensions, ".angle_instanced_arrays");
made the same restore/redraw sequence correctly restore all six divisors to 1, with no failed divisor assignments recorded. The chart still remained blank. Manually restoring divisors through the new extension likewise did not recover the whole chart. Other resource/state restoration problems remain to be isolated; this patch should not be treated as a complete solution.
Related reports
- https://github.com/plotly/plotly.py/issues/3440 describes similar missing traces under WebGL context pressure. Its closure does not establish that this restoration path was fixed.
- https://github.com/plotly/plotly.js/issues/6365 concerns context accumulation during updates, a related source of pressure rather than this stale-reference recovery defect.
- https://github.com/regl-project/regl/pull/511 fixes a different extension-restoration problem involving unsupported optional extensions.
I could not find an existing issue specifically covering this stale compiled extension reference. Please let me know if this should instead be tracked in plotly/regl, or if additional context/resource diagnostics would help.
- Langage dominant
- JavaScript
- Étoiles
- 18.3k
- Forks
- 2k
- Merge moyen
- 2 j 10 h
- PR mergées (30 j)
- 30
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de plotly/plotly.js
-
bug
Difficulté 1/5 Moins d'une heure Accessibilité débutants 85/100
-
chore P3 plotly-internal size: 3 task
Difficulté 2/5 1-3 heures Accessibilité débutants 77/100
-
chore P1 plotly-internal size: 1 task
Difficulté 1/5 Moins d'une heure Accessibilité débutants 82/100
-
chore P3 plotly-internal size: 1 task
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
-
bug
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
Toutes les issues de plotly/plotly.js
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 70/100
-
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
mksglu/context-mode#1200 ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
neondatabase/website#5944 ·
-
module: core
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
bigbluebutton/bigbluebutton#25849 ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
jaegertracing/jaeger-ui#4506 ·