@perspective-dev/viewer-charts 4.5.0 → 4.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/axis/bar-axis.d.ts +9 -1
- package/dist/esm/axis/categorical-axis.d.ts +0 -2
- package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
- package/dist/esm/charts/common/expand-domain.d.ts +20 -0
- package/dist/esm/charts/common/tree-chart.d.ts +7 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
- package/dist/esm/charts/common/tree-interact.d.ts +46 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
- package/dist/esm/charts/series/series-build.d.ts +38 -2
- package/dist/esm/charts/series/series-render.d.ts +1 -4
- package/dist/esm/charts/series/series-type.d.ts +19 -17
- package/dist/esm/charts/series/series.d.ts +16 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
- package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +10 -28
- package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
- package/dist/esm/interaction/zoom-controller.d.ts +31 -20
- package/dist/esm/interaction/zoom-router.d.ts +3 -26
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/plugin.d.ts +15 -3
- package/dist/esm/render/scheduler.d.ts +23 -0
- package/dist/esm/theme/palette.d.ts +0 -5
- package/dist/esm/transport/protocol.d.ts +2 -7
- package/dist/esm/webgl/context-manager.d.ts +100 -117
- package/dist/esm/webgl/context-pool.d.ts +40 -0
- package/dist/esm/webgl/gl-context.d.ts +54 -0
- package/dist/esm/worker/renderer.worker.d.ts +2 -4
- package/package.json +45 -45
- package/src/ts/axis/bar-axis.ts +74 -45
- package/src/ts/axis/categorical-axis.ts +0 -2
- package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
- package/src/ts/charts/candlestick/candlestick.ts +10 -29
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
- package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
- package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
- package/src/ts/charts/cartesian/cartesian.ts +43 -4
- package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
- package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
- package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
- package/src/ts/charts/chart-base.ts +20 -6
- package/src/ts/charts/chart.ts +1 -1
- package/src/ts/charts/common/category-axis-resolver.ts +135 -1
- package/src/ts/charts/common/expand-domain.ts +40 -0
- package/src/ts/charts/common/tree-chart.ts +16 -0
- package/src/ts/charts/common/tree-chrome.ts +86 -1
- package/src/ts/charts/common/tree-interact.ts +209 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
- package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
- package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
- package/src/ts/charts/series/series-build.ts +394 -21
- package/src/ts/charts/series/series-render.ts +159 -38
- package/src/ts/charts/series/series-type.ts +37 -17
- package/src/ts/charts/series/series.ts +63 -68
- package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
- package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
- package/src/ts/charts/sunburst/sunburst.ts +1 -15
- package/src/ts/charts/treemap/treemap-interact.ts +22 -189
- package/src/ts/charts/treemap/treemap-render.ts +19 -46
- package/src/ts/charts/treemap/treemap.ts +1 -16
- package/src/ts/config.ts +21 -1
- package/src/ts/interaction/host-sink-message.ts +33 -22
- package/src/ts/interaction/raw-event-forwarder.ts +10 -12
- package/src/ts/interaction/zoom-controller.ts +120 -83
- package/src/ts/interaction/zoom-router.ts +3 -126
- package/src/ts/map/tile-layer.ts +13 -13
- package/src/ts/plugin/plugin.ts +176 -196
- package/src/ts/render/scheduler.ts +125 -42
- package/src/ts/shaders/line-uniform.frag.glsl +2 -1
- package/src/ts/shaders/line-uniform.vert.glsl +19 -0
- package/src/ts/theme/palette.ts +1 -4
- package/src/ts/transport/protocol.ts +3 -8
- package/src/ts/webgl/context-manager.ts +195 -189
- package/src/ts/webgl/context-pool.ts +78 -0
- package/src/ts/webgl/gl-context.ts +191 -0
- package/src/ts/worker/dispatch.ts +0 -1
- package/src/ts/worker/renderer.worker.ts +72 -65
- package/src/ts/worker/session-host.ts +29 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import { ShaderRegistry } from "./shader-registry";
|
|
14
|
+
import { SHADER_MANIFEST } from "./shader-manifest";
|
|
15
|
+
|
|
16
|
+
export type WebGLCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The unit the browser actually counts against its per-agent WebGL
|
|
20
|
+
* context cap (~16 in Chromium): one canvas + one GL context + the
|
|
21
|
+
* shader programs compiled against it.
|
|
22
|
+
*
|
|
23
|
+
* Factored out of {@link WebGLContextManager} so that *many* managers
|
|
24
|
+
* (one per chart) can share *one* GL context. The browser limit is on
|
|
25
|
+
* contexts, not on the buffers/programs living inside one — a single
|
|
26
|
+
* context can host an arbitrary number of independent
|
|
27
|
+
* `WebGLBuffer`s/programs (bounded only by memory). So N charts can
|
|
28
|
+
* render through K ≪ N contexts as long as each keeps its own
|
|
29
|
+
* `BufferPool` (buffers are keyed by name; two charts would otherwise
|
|
30
|
+
* collide) and renders are serialized per context (the shared drawing
|
|
31
|
+
* buffer can hold one chart's frame at a time). See
|
|
32
|
+
* [context-pool.ts](./context-pool.ts) and the scheduler's per-backend
|
|
33
|
+
* grouping in [../render/scheduler.ts](../render/scheduler.ts).
|
|
34
|
+
*
|
|
35
|
+
* Shaders are shared per context: programs are immutable GLSL once
|
|
36
|
+
* compiled (uniforms are set per draw), so co-tenant charts reuse them
|
|
37
|
+
* and only pay the compile cost once per context.
|
|
38
|
+
*/
|
|
39
|
+
let NEXT_CONTEXT_ID = 1;
|
|
40
|
+
|
|
41
|
+
export class GLContext {
|
|
42
|
+
readonly id: number;
|
|
43
|
+
readonly canvas: WebGLCanvas;
|
|
44
|
+
readonly gl: WebGL2RenderingContext | WebGLRenderingContext;
|
|
45
|
+
readonly isWebGL2: boolean;
|
|
46
|
+
shaders: ShaderRegistry;
|
|
47
|
+
private _maxVertexAttribs: number;
|
|
48
|
+
private _angle: ANGLE_instanced_arrays | null = null;
|
|
49
|
+
private _precompile: boolean;
|
|
50
|
+
private _destroyed = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-tenant hooks fired after a `webglcontextrestored`. Each
|
|
54
|
+
* {@link WebGLContextManager} on this context registers one that
|
|
55
|
+
* rebuilds its `BufferPool` (the shaders are rebuilt here, once, for
|
|
56
|
+
* all tenants). On an intentional {@link destroy} these are dropped
|
|
57
|
+
* without firing.
|
|
58
|
+
*/
|
|
59
|
+
private _onRestore = new Set<() => void>();
|
|
60
|
+
|
|
61
|
+
constructor(canvas: WebGLCanvas, options: { precompile?: boolean } = {}) {
|
|
62
|
+
this.id = NEXT_CONTEXT_ID++;
|
|
63
|
+
this.canvas = canvas;
|
|
64
|
+
this._precompile = options.precompile ?? false;
|
|
65
|
+
const gl2 = canvas.getContext("webgl2", {
|
|
66
|
+
antialias: true,
|
|
67
|
+
alpha: true,
|
|
68
|
+
premultipliedAlpha: false,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (gl2) {
|
|
72
|
+
this.gl = gl2 as WebGL2RenderingContext;
|
|
73
|
+
this.isWebGL2 = true;
|
|
74
|
+
} else {
|
|
75
|
+
const gl1 = canvas.getContext("webgl", {
|
|
76
|
+
antialias: true,
|
|
77
|
+
alpha: true,
|
|
78
|
+
premultipliedAlpha: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!gl1) {
|
|
82
|
+
throw new Error("WebGL is not supported");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.gl = gl1 as WebGLRenderingContext;
|
|
86
|
+
this.isWebGL2 = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.shaders = new ShaderRegistry(this.gl);
|
|
90
|
+
if (this._precompile) {
|
|
91
|
+
this.shaders.precompile(SHADER_MANIFEST);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._maxVertexAttribs = this.gl.getParameter(
|
|
95
|
+
this.gl.MAX_VERTEX_ATTRIBS,
|
|
96
|
+
) as number;
|
|
97
|
+
if (!this.isWebGL2) {
|
|
98
|
+
this._angle = this.gl.getExtension("ANGLE_instanced_arrays");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Both `HTMLCanvasElement` and `OffscreenCanvas` dispatch
|
|
102
|
+
// `webglcontextlost` / `webglcontextrestored` on the canvas.
|
|
103
|
+
(canvas as EventTarget).addEventListener(
|
|
104
|
+
"webglcontextlost",
|
|
105
|
+
(e: Event) => {
|
|
106
|
+
// Don't reserve the slot for a restore once we've
|
|
107
|
+
// intentionally torn the context down — `destroy()`
|
|
108
|
+
// calls `loseContext()`, which fires this event.
|
|
109
|
+
if (this._destroyed) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
(canvas as EventTarget).addEventListener("webglcontextrestored", () => {
|
|
118
|
+
// Rebuild the shared program cache once for the whole
|
|
119
|
+
// context, then let each tenant rebuild its own buffers.
|
|
120
|
+
this.shaders.releaseAll();
|
|
121
|
+
this.shaders = new ShaderRegistry(this.gl);
|
|
122
|
+
if (this._precompile) {
|
|
123
|
+
this.shaders.precompile(SHADER_MANIFEST);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const cb of this._onRestore) {
|
|
127
|
+
cb();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Return every vertex-attribute slot to its default state: disabled,
|
|
134
|
+
* with a zero instance divisor. Vertex-array state (enable flags,
|
|
135
|
+
* pointers, divisors) is global to a context's default VAO — there
|
|
136
|
+
* are no per-chart VAOs in this codebase — so when many charts share
|
|
137
|
+
* one pooled context, a slot a prior chart enabled stays enabled for
|
|
138
|
+
* the next chart, now pointing at a buffer the prior chart's
|
|
139
|
+
* teardown deleted. Its next `drawArraysInstanced` then fails GL
|
|
140
|
+
* validation: "no buffer is bound to enabled attribute", silently
|
|
141
|
+
* dropping the draw.
|
|
142
|
+
*
|
|
143
|
+
* Glyphs already (re)enable + bind + set the divisor for every slot
|
|
144
|
+
* they use on each frame, so clearing to the default here is a safe
|
|
145
|
+
* baseline — it only removes the *leftover* enables a sibling chart
|
|
146
|
+
* left behind. Called from {@link WebGLContextManager.beginFrame} in
|
|
147
|
+
* shared (pooled) mode only; the 1:1 default never shares a context
|
|
148
|
+
* and so keeps its original, reset-free path.
|
|
149
|
+
*/
|
|
150
|
+
resetVertexArrayState(): void {
|
|
151
|
+
const gl = this.gl;
|
|
152
|
+
for (let i = 0; i < this._maxVertexAttribs; i++) {
|
|
153
|
+
gl.disableVertexAttribArray(i);
|
|
154
|
+
if (this.isWebGL2) {
|
|
155
|
+
(gl as WebGL2RenderingContext).vertexAttribDivisor(i, 0);
|
|
156
|
+
} else {
|
|
157
|
+
this._angle?.vertexAttribDivisorANGLE(i, 0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register a tenant's post-restore rebuild hook. Returns a disposer.
|
|
164
|
+
*/
|
|
165
|
+
onRestore(cb: () => void): () => void {
|
|
166
|
+
this._onRestore.add(cb);
|
|
167
|
+
return () => this._onRestore.delete(cb);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* True once lost — either our own {@link destroy} or a browser
|
|
172
|
+
* eviction of the oldest context past the per-agent cap.
|
|
173
|
+
*/
|
|
174
|
+
isContextLost(): boolean {
|
|
175
|
+
return this._destroyed || this.gl.isContextLost();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get destroyed(): boolean {
|
|
179
|
+
return this._destroyed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
destroy(): void {
|
|
183
|
+
this._destroyed = true;
|
|
184
|
+
this._onRestore.clear();
|
|
185
|
+
this.shaders.releaseAll();
|
|
186
|
+
const ext = this.gl.getExtension("WEBGL_lose_context");
|
|
187
|
+
if (ext) {
|
|
188
|
+
ext.loseContext();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -16,6 +16,8 @@ import type { Client, Table, View } from "@perspective-dev/client";
|
|
|
16
16
|
|
|
17
17
|
import type * as wasm_module_type from "@perspective-dev/viewer/dist/wasm/perspective-viewer.js";
|
|
18
18
|
import { WebGLContextManager } from "../webgl/context-manager";
|
|
19
|
+
import { ContextPool } from "../webgl/context-pool";
|
|
20
|
+
import { RENDER_CONTEXT_POOL_SIZE } from "../config";
|
|
19
21
|
import { ChartImplementation } from "../charts/chart";
|
|
20
22
|
import { ZoomController } from "../interaction/zoom-controller";
|
|
21
23
|
import {
|
|
@@ -37,7 +39,7 @@ import { viewToColumnDataMap } from "../data/view-reader";
|
|
|
37
39
|
import { loadFontDeduped } from "./font-loader";
|
|
38
40
|
import { dispatch } from "./dispatch";
|
|
39
41
|
import { installSessionHost } from "./session-host";
|
|
40
|
-
import { deferIfDraining } from "../render/scheduler";
|
|
42
|
+
import { deferIfDraining, unregister } from "../render/scheduler";
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Sentinel thrown inside the `with_typed_arrays` callback when a newer
|
|
@@ -76,6 +78,25 @@ async function resolveChartImpl(
|
|
|
76
78
|
return await factory();
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Renderer-scope shared context pool for pooled blit mode. Lazily
|
|
83
|
+
* created on first use so non-blit / non-pooled scopes never allocate
|
|
84
|
+
* one. One per renderer scope (the worker, or the main thread in
|
|
85
|
+
* in-process mode) — `WorkerRenderer`s in the same scope share its K
|
|
86
|
+
* contexts.
|
|
87
|
+
*/
|
|
88
|
+
let CONTEXT_POOL: ContextPool | null = null;
|
|
89
|
+
|
|
90
|
+
function getContextPool(precompile: boolean): ContextPool {
|
|
91
|
+
if (!CONTEXT_POOL) {
|
|
92
|
+
CONTEXT_POOL = new ContextPool(RENDER_CONTEXT_POOL_SIZE, {
|
|
93
|
+
precompile,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return CONTEXT_POOL;
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
export class WorkerRenderer {
|
|
80
101
|
chartImpl: ChartImplementation;
|
|
81
102
|
glManager: WebGLContextManager;
|
|
@@ -131,20 +152,35 @@ export class WorkerRenderer {
|
|
|
131
152
|
|
|
132
153
|
this.chartImpl = new ImplClass();
|
|
133
154
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
// Three surfaces, by mode:
|
|
156
|
+
// - direct: the host's transferred `.webgl-canvas` (1:1 with a
|
|
157
|
+
// context, permanently).
|
|
158
|
+
// - pooled blit: borrow one of K shared contexts so live-
|
|
159
|
+
// context count stays bounded regardless of chart count.
|
|
160
|
+
// - unpooled blit: a private offscreen surface per chart.
|
|
161
|
+
// Blit modes omit `msg.glCanvas`; the renderer ships each frame
|
|
162
|
+
// as an `ImageBitmap` via the `endFrame` callback wired below.
|
|
163
|
+
const pooled =
|
|
164
|
+
msg.renderMode === "blit" &&
|
|
165
|
+
!msg.glCanvas &&
|
|
166
|
+
RENDER_CONTEXT_POOL_SIZE > 0;
|
|
167
|
+
|
|
168
|
+
if (pooled) {
|
|
169
|
+
this.glManager = new WebGLContextManager(
|
|
170
|
+
getContextPool(msg.precompileShaders ?? false).acquire(),
|
|
143
171
|
);
|
|
172
|
+
} else {
|
|
173
|
+
const glCanvas =
|
|
174
|
+
msg.glCanvas ??
|
|
175
|
+
new OffscreenCanvas(
|
|
176
|
+
Math.max(1, Math.round(msg.cssWidth * msg.dpr)),
|
|
177
|
+
Math.max(1, Math.round(msg.cssHeight * msg.dpr)),
|
|
178
|
+
);
|
|
144
179
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
this.glManager = new WebGLContextManager(glCanvas, {
|
|
181
|
+
precompile: msg.precompileShaders ?? false,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
148
184
|
|
|
149
185
|
if (msg.renderMode === "blit") {
|
|
150
186
|
this.glManager.setFrameCallback((bitmap) => {
|
|
@@ -177,41 +213,11 @@ export class WorkerRenderer {
|
|
|
177
213
|
this.chartImpl.setView?.(view);
|
|
178
214
|
this.glManager.bufferPool.maxCapacity = msg.bufferMaxCapacity;
|
|
179
215
|
this.glManager.resize(msg.cssWidth, msg.cssHeight, msg.dpr);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
lines: envelope.payload.lines,
|
|
186
|
-
pos: envelope.payload.pos,
|
|
187
|
-
bounds: envelope.payload.bounds,
|
|
188
|
-
});
|
|
189
|
-
break;
|
|
190
|
-
case "dismiss":
|
|
191
|
-
this.post({ kind: "dismissTooltip" });
|
|
192
|
-
break;
|
|
193
|
-
case "setCursor":
|
|
194
|
-
this.post({ kind: "setCursor", cursor: envelope.cursor });
|
|
195
|
-
break;
|
|
196
|
-
case "userClick":
|
|
197
|
-
this.post({
|
|
198
|
-
kind: "userClick",
|
|
199
|
-
detail: envelope.payload as any,
|
|
200
|
-
});
|
|
201
|
-
break;
|
|
202
|
-
case "userSelect":
|
|
203
|
-
this.post({
|
|
204
|
-
kind: "userSelect",
|
|
205
|
-
selected: envelope.payload.selected,
|
|
206
|
-
row: envelope.payload.row,
|
|
207
|
-
column_names: envelope.payload.column_names,
|
|
208
|
-
insertConfig: envelope.payload.insertConfig as any,
|
|
209
|
-
});
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
this.chartImpl.attachTooltip?.(hostSink);
|
|
216
|
+
// `MessageHostSink` emits `WorkerMsg`s directly — no translation
|
|
217
|
+
// needed here.
|
|
218
|
+
this.chartImpl.attachTooltip?.(
|
|
219
|
+
new MessageHostSink((msg) => this.post(msg)),
|
|
220
|
+
);
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
setViewByName(name: string): void {
|
|
@@ -309,13 +315,9 @@ export class WorkerRenderer {
|
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
317
|
} catch (err) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// this method). Surface to the worker console; the host's
|
|
316
|
-
// pending promise still gets resolved via the `finally`
|
|
317
|
-
// ack below so `draw()` can't deadlock on a renderer error.
|
|
318
|
-
console.error("loadAndRender failed", err);
|
|
318
|
+
if ((err + "").indexOf("View not found") === -1) {
|
|
319
|
+
console.error("loadAndRender failed", err);
|
|
320
|
+
}
|
|
319
321
|
} finally {
|
|
320
322
|
this.post({ kind: "loadAndRenderAck", msgId: msg.msgId });
|
|
321
323
|
}
|
|
@@ -417,10 +419,8 @@ export class WorkerRenderer {
|
|
|
417
419
|
|
|
418
420
|
/**
|
|
419
421
|
* Hit-test the cursor against the chart's facet grid (in faceted
|
|
420
|
-
* mode) or its current layout (single-plot).
|
|
421
|
-
*
|
|
422
|
-
* worker owns the facet grid and controllers, so the resolution
|
|
423
|
-
* runs here.
|
|
422
|
+
* mode) or its current layout (single-plot). The worker owns the
|
|
423
|
+
* facet grid and controllers, so the resolution runs here.
|
|
424
424
|
*/
|
|
425
425
|
private _resolveTarget(mx: number, my: number): ZoomTarget | null {
|
|
426
426
|
const chart = this.chartImpl as any;
|
|
@@ -564,12 +564,14 @@ export class WorkerRenderer {
|
|
|
564
564
|
* Composite the three layers into a single PNG `Blob`.
|
|
565
565
|
*/
|
|
566
566
|
async snapshotPng(): Promise<Blob> {
|
|
567
|
-
// Snapshot bypasses the scheduler's drain, so it must
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
|
|
567
|
+
// Snapshot bypasses the scheduler's drain, so it must mirror
|
|
568
|
+
// the per-frame prep itself — apply any pending resize, and in
|
|
569
|
+
// pooled blit mode size the shared canvas to this chart and
|
|
570
|
+
// reset shared GL state — otherwise a snapshot taken after a
|
|
571
|
+
// resize message but before the next drain (or after a co-tenant
|
|
572
|
+
// chart left the shared canvas at its own size) would render at
|
|
573
|
+
// the wrong dimensions.
|
|
574
|
+
this.glManager.beginFrame();
|
|
573
575
|
this.chartImpl._fullRender(this.glManager);
|
|
574
576
|
const gl = this.glManager.gl;
|
|
575
577
|
const glCanvas = gl.canvas as OffscreenCanvas;
|
|
@@ -610,6 +612,11 @@ export class WorkerRenderer {
|
|
|
610
612
|
}
|
|
611
613
|
|
|
612
614
|
destroy(): void {
|
|
615
|
+
// Drop any frame this manager still has queued in the
|
|
616
|
+
// module-level scheduler *before* losing its GL context, so a
|
|
617
|
+
// next-RAF `drain()` can't paint/present against a dead
|
|
618
|
+
// context (the "scheduler: present failed" path).
|
|
619
|
+
unregister(this.glManager);
|
|
613
620
|
this.chartImpl.destroy();
|
|
614
621
|
this.glManager.destroy();
|
|
615
622
|
}
|
|
@@ -21,6 +21,20 @@ import { dispatch } from "./dispatch";
|
|
|
21
21
|
|
|
22
22
|
const RENDERERS = new Map<number, WorkerRenderer>();
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Sessions whose `destroy` arrived before their async `bootstrap`
|
|
26
|
+
* resolved. Bootstrap does multiple awaits (wasm `initSync`, font
|
|
27
|
+
* loads, `open_table`, lazy chart-impl import); a host that connects
|
|
28
|
+
* then immediately disconnects a viewer can post `init` and `destroy`
|
|
29
|
+
* back-to-back, with `destroy` landing while the renderer is still
|
|
30
|
+
* being built. The plain `RENDERERS.get(sessionId)` lookup in the
|
|
31
|
+
* destroy branch would miss it and the bootstrap would then register a
|
|
32
|
+
* renderer (and its WebGL context) that nothing ever tears down —
|
|
33
|
+
* leaking one context per raced toggle until the browser evicts the
|
|
34
|
+
* oldest. We tombstone the id here and honor it when bootstrap lands.
|
|
35
|
+
*/
|
|
36
|
+
const PENDING_DESTROY = new Set<number>();
|
|
37
|
+
|
|
24
38
|
function postSession(
|
|
25
39
|
sessionId: number,
|
|
26
40
|
msg: WorkerMsg,
|
|
@@ -89,9 +103,18 @@ export function installSessionHost(
|
|
|
89
103
|
|
|
90
104
|
bootstrap(msg as InitMsg, makeSessionPort(sessionId))
|
|
91
105
|
.then((r) => {
|
|
106
|
+
// A `destroy` raced ahead of bootstrap completing —
|
|
107
|
+
// honor it now instead of leaking the freshly-built
|
|
108
|
+
// renderer + its WebGL context.
|
|
109
|
+
if (PENDING_DESTROY.delete(sessionId)) {
|
|
110
|
+
dispatch(r, { kind: "destroy" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
RENDERERS.set(sessionId, r);
|
|
93
115
|
})
|
|
94
116
|
.catch((err) => {
|
|
117
|
+
PENDING_DESTROY.delete(sessionId);
|
|
95
118
|
postSession(sessionId, {
|
|
96
119
|
kind: "error",
|
|
97
120
|
message: String(err),
|
|
@@ -105,6 +128,12 @@ export function installSessionHost(
|
|
|
105
128
|
if (r) {
|
|
106
129
|
dispatch(r, msg);
|
|
107
130
|
RENDERERS.delete(sessionId);
|
|
131
|
+
} else {
|
|
132
|
+
// Renderer not registered yet — bootstrap is still in
|
|
133
|
+
// flight. Tombstone the id so the bootstrap `.then`
|
|
134
|
+
// tears it down on arrival instead of registering a
|
|
135
|
+
// leaked context.
|
|
136
|
+
PENDING_DESTROY.add(sessionId);
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
return;
|