Performance and accessibility checks

The 0.8 qualification (issue #68) asks for measured performance and keyboard-accessible workflows. Its budgets are frozen by the maintainer from measurements like the one below, before anything is optimised. This page describes the tools that produce those measurements, and records the first.

The corpus

tests/data/corpus/ holds a versioned, public-safe set of drawings as .vsk documents. manifest.json records each case’s SHA-256 and what it holds, so a measurement names exactly what it measured:

Case

Kind

Atoms

Species

Reactions

small-ethanol

small

3

1

0

small-stereochemistry

small

13

5

0

small-mechanism

small

12

3

1

small-three-step

small

40

6

3

large-peptide

large

201

1

0

large-scheme-30

large

1,020

31

30

large-chain-2000

large

2,000

1

0

crowded-numbered-60

crowded

534

60

0

crowded-grid-150

crowded

2,685

150

0

The small cases are the manual’s example documents; scripts/make_corpus.py generates the others deterministically and checks the files (--check). Changing a case means a new corpus version, and measurements of different versions are not compared. The test suite checks the manifest and reopens every case through .vsk and QVF with the same species and reactions, so work on speed cannot quietly change graph or stereo fidelity.

The harness

python scripts/benchmark.py --output perf.json
python scripts/benchmark.py --case large-scheme-30 --repeat 10

For each case it times loading, deriving species and reactions, cleanup, Tidy scheme (when there are reactions), SVG, PNG and PDF rendering and QVF export: once cold and --repeat times warm (5 by default), keeping every observation, with the peak memory Python allocated in a separate run (tracemalloc, which leaves out what RDKit allocates in C++). An operation that does not apply or whose optional part is not installed is recorded as skipped, a deliberate refusal as refused, and anything else as a failure with a nonzero exit. The record names the operating system, architecture, processor model, logical CPUs, memory, Python, RDKit, vibe-sketch and corpus versions, never the machine’s name.

First measurement (2026-10-22)

Apple M4 Pro, 12 logical CPUs, 24 GiB, macOS (Darwin 27.0), Python 3.14.8, RDKit 2026.09.1, vibe-sketch 0.3.0 (main), corpus version 1; warm medians of three runs, in seconds:

Case

Load

Derive

Cleanup

Tidy

SVG

PNG

PDF

QVF

small-ethanol

0.001

0.001

0.001

–

0.000

0.004

0.000

0.006

small-stereochemistry

0.001

0.004

0.010

–

0.001

0.033

0.004

0.067

small-mechanism

0.000

0.002

0.003

0.029

0.002

0.010

0.002

0.014

small-three-step

0.001

0.010

0.033

0.046

0.002

0.072

0.026

0.033

large-peptide

0.002

0.023

0.216

–

0.019

0.148

0.037

0.048

large-scheme-30

0.007

0.107

0.463

0.877

0.070

1.459

0.160

0.383

large-chain-2000

0.012

0.228

20.928

–

0.153

refused

refused

0.933

crowded-numbered-60

0.005

0.143

0.394

–

0.036

0.405

0.108

0.321

crowded-grid-150

0.021

0.221

1.204

–

0.138

1.062

0.217

1.142

What it showed:

  • Everything but cleanup of the 2,000-atom chain stayed near or below 1.5 s. That cleanup took about 21 s and peaked at about 217 MB of Python memory.

  • The chain’s PNG and PDF are refused: the figure would be wider than the 14,400 pt publication limit. That refusal is by design; SVG and QVF work.

The maintainer judged that cleanup over budget. Profiling showed that most of it went to three whole-molecule layouts tried one after another, whose cost grows faster than the molecule. Per candidate, in seconds (rdkit / mimic / coordgen), they took 0.10 / 0.08 / 0.13 at 500 atoms, 0.72 / 0.47 / 0.60 at 1,000 and 5.2 / 1.8 / 2.1 at 2,000. The overlap count built an n × n matrix, about 130 MB at 2,000 atoms. Cleanup now:

  • lays out a molecule of more than 500 atoms with the distance-matrix “mimic” candidate alone, which is the fastest at that size and follows the drawing most closely (on the chain all three tie, and mimic was the one chosen);

  • leaves a molecule of more than 3,000 atoms as drawn, saying why (mimic alone takes 3.5 s at 3,000 atoms and 10 s at 5,000);

  • counts overlaps row by row, in memory linear in the atoms.

After the cleanup limits (2026-10-22)

The same machine and corpus, warm medians of three runs, in seconds:

Case

Load

Derive

Cleanup

Tidy

SVG

PNG

PDF

QVF

small-ethanol

0.000

0.001

0.001

–

0.000

0.001

0.000

0.004

small-stereochemistry

0.001

0.003

0.003

–

0.001

0.010

0.002

0.017

small-mechanism

0.001

0.002

0.003

0.010

0.001

0.005

0.001

0.011

small-three-step

0.001

0.004

0.010

0.026

0.002

0.029

0.008

0.021

large-peptide

0.002

0.005

0.110

–

0.009

0.080

0.015

0.028

large-scheme-30

0.007

0.058

0.221

0.388

0.028

0.708

0.082

0.164

large-chain-2000

0.010

0.113

2.535

–

0.080

refused

refused

0.400

crowded-numbered-60

0.004

0.047

0.130

–

0.018

0.189

0.043

0.152

crowded-grid-150

0.017

0.113

0.592

–

0.067

0.499

0.113

0.442

The chain’s cleanup now takes 2.5 s instead of about 21 s, and its peak Python memory is 64 MB instead of 217 MB. It still cleans, with the same result, and the smaller cases are unchanged. The 50-atom cleanup target of #18 is met by a wide margin. One run on one machine is not a budget: the maintainer freezes budgets from records like these.

Keyboard and screen readers

The browser shell’s tests check, in a real browser:

  • the WCAG 2.2 A and AA rules axe-core can test, on the shell and its panels in several states (tests/test_browser_accessibility.py);

  • menus, the tool bar and the panel by keyboard alone, with focus kept where the user was when the panel updates;

  • that every control the Tab key reaches shows a 2 px focus ring;

  • that information is announced politely and fades, while an error interrupts screen readers (role="alert") and stays until the next message or a click, so nobody has to read it in time;

  • a whole session by keyboard alone: import a structure, generate its 3D structure, submit a calculation and cancel it, save with Ctrl+S, export through the Export menu and open a file, each control reached with Tab from where the last one left the focus;

  • that Tab and Shift+Tab both leave the canvas, in either palette.

Canvas limitations

The drawing canvas is Ketcher’s (EPAM). Where it falls short, the shell offers another way:

Limitation

What to use instead

Atoms and bonds are drawn with a pointer; there is no way to place them by keyboard.

Import ▾ → SMILES…, Name… or File…, all by keyboard; or the SDK, CLI, REST API and MCP tools.

A screen reader cannot read the drawing itself.

The Properties panel lists each species with its formula, compound number, multiplicity and name, and each reaction with its sides; Skip to properties and inputs jumps there. vibe-sketch info and the REST API report the same.

Ketcher’s palettes cycle Tab among themselves, and its clipboard area takes the focus back on Shift+Tab, which would trap a keyboard user.

The shell moves the focus itself: Tab and Shift+Tab pass through the canvas’s controls and out of it.

Ketcher’s palettes add about 47 Tab stops.

The compact palette (?palette=compact) shows the shell’s own tool bar instead: one Tab stop, arrow keys inside it.

Ketcher’s canvas is outside the axe-core audit, which cannot judge an upstream drawing surface.

The shell around it, the panels and the menus are audited.

A manual review with a screen reader is still to come, as #68 asks.