Skip to content

fix: Share single Deno/V8 worker thread#237

Merged
jonmmease merged 2 commits intomainfrom
jonmmease/single-thread
Feb 13, 2026
Merged

fix: Share single Deno/V8 worker thread#237
jonmmease merged 2 commits intomainfrom
jonmmease/single-thread

Conversation

@jonmmease
Copy link
Collaborator

@jonmmease jonmmease commented Feb 13, 2026

Summary

Share a single Deno/V8 worker thread across all VlConverter instances, fixing segfaults caused by multiple V8 isolates coexisting in the same process (#206). Adds font configuration version tracking so dynamically-registered fonts propagate to long-lived canvas contexts, and a worker respawn mechanism so the process can recover from worker thread failures.

Motivation

Previously, each VlConverter::new() spawned a dedicated worker thread with its own V8 isolate. This caused segfaults when multiple VlConverter instances existed simultaneously (#206), because V8 isolates cannot safely coexist in the same process.

Changes

Core architecture (vl-convert-rs/src/converter.rs, +214/-118)

  • New VlConverterRuntime struct holds a Sender<VlConvertCommand> and the worker thread JoinHandle
  • VL_CONVERTER_RUNTIME is a static Mutex<Option<VlConverterRuntime>> so the worker can be respawned if it exits
  • get_or_spawn_sender() checks if the worker is alive via JoinHandle::is_finished(); if the worker has exited, it spawns a fresh one transparently
  • spawn_worker_thread() extracted as a standalone function (was previously inlined in lazy_static!)
  • VlConverter no longer caches a sender — each method call goes through get_or_spawn_sender(), ensuring it always talks to a live worker
  • Commands dispatched via bounded mpsc channel (capacity 32), processed sequentially by the worker

Error resilience

  • VlConvertCommand::send_error() method forwards errors to the command's oneshot responder regardless of variant type
  • refresh_font_config_if_needed errors in the worker loop are caught, sent to the current command's responder, and the loop continues (previously the ? operator killed the worker thread)

Font version propagation

An AtomicU64 counter (FONT_CONFIG_VERSION) is incremented when fonts are registered via register_font_directory(). The worker thread and individual canvas contexts each track the last-seen version, refreshing their font databases lazily when a mismatch is detected. This ensures dynamically-registered fonts are available even in long-lived canvas contexts that Vega caches internally.

op_canvas_set_font now calls refresh_canvas_fonts_if_needed, matching the pattern in the other text ops (measureText, fillText, strokeText, etc.). This ensures newly-registered fonts are available when Vega sets a font, not just when it measures or renders text.

Supporting changes

  • SharedFontConfig changed from tuple struct to named fields (adds version: u64)
  • CanvasResource::new() accepts initial font config version
  • Canvas2dContext::update_font_database() method for hot-swapping fonts on existing contexts

Review Tour

  • vl-convert-rs/src/converter.rs — The core architectural change. Search for fn spawn_worker_thread() for the worker loop, fn get_or_spawn_sender() for the respawn mechanism, and impl VlConverter for the simplified public API. VlConvertCommand::send_error() is near the enum definition at the bottom.
  • vl-convert-rs/src/text.rs — Global FONT_CONFIG_VERSION atomic counter, incremented in register_font_directory
  • vl-convert-canvas2d-deno/src/ops/text.rs — Per-canvas refresh_canvas_fonts_if_needed() helper that propagates font changes from the shared config to individual canvas contexts. Called in 6 text ops (including op_canvas_set_font).
  • vl-convert-canvas2d-deno/src/lib.rsSharedFontConfig struct evolution (tuple to named fields with version)
  • vl-convert-canvas2d-deno/src/ops/mod.rsop_canvas_create captures font version
  • vl-convert-canvas2d-deno/src/resource.rsCanvasResource gains font_config_version field
  • vl-convert-canvas2d/src/context/mod.rsupdate_font_database() method on Canvas2dContext

Testing

Existing test suite passes (117 tests via cargo test -p vl-convert-rs -- --test-threads=1). The architectural change is transparent to the public API — all existing conversions produce identical output.

New test test_font_version_propagation verifies that register_font_directory increments the version counter and that the worker thread survives the font config refresh, confirming the 3-layer version propagation mechanism works end-to-end.

jonmmease and others added 2 commits February 13, 2026 12:25
Move the worker thread + command dispatch loop into a lazy_static
global (VL_CONVERTER_RUNTIME) so every VlConverter::new() reuses the
same V8 isolate. This eliminates segfaults from multiple V8 isolates
running across threads (#206).

Font config changes (register_font_directory) are tracked with an
atomic version counter (FONT_CONFIG_VERSION). Before each command the
worker checks the version and refreshes SharedFontConfig in OpState.
To handle Vega's internally cached text-measurement canvas, the canvas
text ops (measureText, fillText, strokeText) also compare the version
and hot-swap the FontSystem when fonts have changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t font refresh

- Replace lazy_static singleton with Mutex<Option<VlConverterRuntime>> so the
  worker thread can be respawned if it exits unexpectedly
- Add get_or_spawn_sender() that checks JoinHandle::is_finished() and respawns
  transparently; VlConverter no longer caches a sender
- Extract spawn_worker_thread() from the lazy_static initializer block
- Catch refresh_font_config_if_needed errors per-command via
  VlConvertCommand::send_error() instead of killing the worker with ?
- Add refresh_canvas_fonts_if_needed to op_canvas_set_font
- Add test_font_version_propagation test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jonmmease jonmmease changed the title Share single Deno/V8 worker thread, add worker respawn and font version tracking fix: Share single Deno/V8 worker thread Feb 13, 2026
@jonmmease jonmmease marked this pull request as ready for review February 13, 2026 18:43
This was referenced Feb 13, 2026
@jonmmease jonmmease merged commit 16975c4 into main Feb 13, 2026
12 checks passed
@jonmmease jonmmease deleted the jonmmease/single-thread branch February 13, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant