sqlit performance study

Abstract

A measured investigation of sqlit identifies a severe autocomplete bottleneck and smaller sources of unnecessary startup and idle work. The strongest implemented result is a 98.1% reduction in completion work plus dropdown refresh with 5,000 stored routines: the median falls from 1,841.2 ms to 34.6 ms. An index replaces a quadratic scan without changing the observed suggestions. Empty-queue scheduling changes reduce explorer-focus idle CPU by 72.0%, and honoring a saved disabled-worker preference reduces launch-and-exit CPU by 16.1%.

The study contains 1349 recorded observations from 152 sequential jobs, plus diagnostic pilots. It exercises the actual Textual application in pseudo-terminals, five real database engines, a controlled network-delay relay, and alternative rendering and serialization strategies. Database experiments show approximately 98% less response payload when a 50,000-row query is explicitly bounded to the first 1,001 rows. Bulk rendering and threaded preparation finish sooner but produce longer event-loop stalls. A sampled, row-backed prototype is promising for CPU and result availability, with substantial feature-contract work remaining.

Default first-refresh startup improvement is not established by the confidence interval. The long-cell change also lacks a reliable end-to-end timing gain in the repeated trials. Results apply to the stated fixtures, software versions and machine; they are not an overall application speedup, a physical FPS measurement or a production-cloud benchmark.

1. Study design

The investigation separates time to first useful display, time to complete a result, consumed CPU, event-loop responsiveness, terminal output, database payload and process memory. These quantities answer different questions. An optimization that removes waiting can increase CPU or redraw traffic, while a process that consumes little CPU can still block input. Each experimental conclusion names its measurement boundary.

Baseline source9db49c230c7c, main at the start of the study.
Measured changes0640c96464dc, including 1efa0e8.
MachineIntel Core Ultra 5 228V, eight logical CPUs, approximately 31 GiB RAM, Linux. Existing CPU-governor settings were retained.
SoftwareCPython 3.13.5; Textual 8.2.8; textual-fastdatatable 0.19.0; PyArrow 21.0.0. Dependencies came from the existing frozen lockfile.
UI environment120 × 40 pseudo-terminal; isolated configuration and temporary paths; synthetic application stores; real rendering and terminal writes.
Database enginesPostgreSQL 16, MySQL 8.0 and MariaDB 11 in disposable containers; SQLite and DuckDB in temporary files. Image content hashes are retained in the observations.
Measurement date10 September 2026. Source, package inventory, workload scripts, image hashes and run receipts accompany the results.

Experimental coverage

QuestionExperimentReplication and boundary
Startup and import costDefault worker policy, saved worker disabled, and empty bytecode cache15 launches per source and normal condition; seven empty-bytecode launches. Separate import-traced pilot.
Typing-related freezes100–5,000 routines in the completion engine; 1,000 and 5,000 routines in the app dropdown pathNine trials per condition. CPU-function warmups are excluded; UI trials use fresh app processes.
Idle CPUExplorer focus and editor focus, with blinking preservedNine five-second windows per focus and source. The high-frequency heartbeat is disabled here.
Results and scrolling1,000–50,000 rows, six-column data, 40-column data, long text, decimals, 30 scroll actions and filteringFive trials per rendering strategy in a fixed workload sequence. Long-cell isolation adds five fresh-process trials per source.
Network and database costRow caps, explicit LIMIT, streaming cleanup, metadata query counts and connection reuseNine repeats across three server engines at zero and 20 ms added one-way latency. SQL runs against real servers.
Process and serialization costFresh/reused local connections, cancellable queries, cold/warm workers, 1,000/50,000 rows and IPC representationsNine repeats. Python-to-Python and already-columnar boundaries are reported separately.

Controls and statistics

Workloads ran sequentially. Baseline/candidate order was randomized within trial blocks with a fixed seed, and the rendering-strategy order was randomized. Database scenarios were shuffled within each repetition. Source commits were checked before jobs. Docker servers were limited to one CPU and 512 MiB each, exposed only on random loopback ports, then stopped and removed. Only synthetic database contents and public lab credentials were used.

The principal statistic is the median. Error bars show a nonparametric 95% bootstrap interval for that median, using 2,000 resamples. Reported improvement intervals use 5,000 resamples; matched trial blocks are resampled together where available. The completion-engine source runs use independent resampling. Reduction is 100 × (median before − median after) / median before. Negative reduction means increased cost. Means, standard deviations, quartiles, minimum, maximum and descriptive p95 values are available in the interactive appendix and JSON.

These are exploratory intervals, without correction for examining many alternatives. A p95 computed from five or nine runs is a description of this small sample, not a service-level guarantee. No host cache was dropped, CPU governor changed, or visible desktop manipulated. A new Python process is “cold” only at the process level; the filesystem may remain warm. The empty-bytecode condition redirects Python’s bytecode lookup and does not emulate a cold disk.

2. Diagnostics and calibration

The existing startup profiler and import tracer identified the startup phases. The existing debug-event bus and UI-stall watchdog were exercised in dedicated diagnostic runs. A known 120 ms blocking sleep produced a roughly 118 ms excess-delay observation and a watchdog warning. This control matters because the blocking sleep consumes little CPU: CPU utilization alone would miss the freeze.

The new lab adds a five-millisecond asyncio heartbeat, per-workload consumed CPU, terminal-byte accounting, display-call timestamps, Chrome trace exports, repeatable result assertions and statistics. cProfile localized rendering work in the compositor, table and Rich formatting paths. An independent py-spy recording of the baseline completion workload collected 471 samples with no sampling errors and retained a Speedscope file. Diagnostic timings are excluded from the formal comparative summaries.

A heartbeat value is lateness beyond its requested five-millisecond sleep. It is not a measured frame presentation time. Likewise, an application display call can write a partial update and need not correspond to one monitor frame. The report therefore uses event-loop lateness and terminal output as responsiveness evidence, without relabeling them as physical FPS. The formal completion runs disable the native watchdog to keep diagnostics overhead separate; watchdog evidence comes from the dedicated control runs.

The existing rendering tests permit several seconds and the standard headless app path does not start the idle scheduler. Those tests remain useful correctness checks, but cannot by themselves establish smooth interactive performance. The PTY experiments run the ordinary scheduler and renderer. A CLI preparser issue also surfaced: an absolute value following a diagnostic path flag could be mistaken for a project directory. The branch repairs that parsing and adds path-argument regressions.

Clock and CPU definitions follow Python’s time API. Threaded work must also respect the Textual worker/UI boundary; moving a function to a thread is not evidence that its complete pipeline has become non-blocking.

3. Autocomplete: a confirmed quadratic bottleneck

Implemented and tested

The baseline computes a display name for each routine by scanning every routine again to discover same-name entries in other schemas or databases. With N routines, this creates approximately candidate comparisons before returning at most 50 suggestions. The work also occurs for unrelated SQL and, in the original ordering, even before the blank-input early return.

The change builds one mapping from lowercased routine name to its database/schema identities, then uses that mapping for disambiguation. Original spelling, qualified names and output ordering are preserved. Blank or string-literal input returns before constructing the catalog index. The index is local to the completion request, avoiding a persistent cross-connection invalidation problem.

image/svg+xml sqlit performance laboratory 100 1000 5000 Stored routines 1 0 1 1 0 0 1 0 1 1 0 2 1 0 3 Completion function (ms) Algorithm scaling; n = 9 per size Baseline Implemented changes Baseline Implemented 0 250 500 750 1000 1250 1500 1750 App completion + dropdown refresh (ms) 1,841.2 ms 34.6 ms Actual PTY app; 5,000 routines; n = 9
Figure 1. Catalog scaling and the actual app completion path. Shading and error bars show 95% bootstrap intervals for the median. Input transport and the existing 100 ms debounce are outside this timing boundary. Download SVG

For 5,000 routines, the standalone completion function falls from 2,337.5 ms to 5.6 ms. In the real application completion-and-dropdown path, the corresponding median is 1,841.2 ms to 34.6 ms, with a 95% reduction interval of approximately 97.0–98.2%. At 1,000 routines the UI path falls from 98.4 to 21.8 ms.

The large difference between the optimized function and the complete dropdown path is residual UI work, not a contradiction. The UI also formats names, computes context, mounts suggestion rows and refreshes. The existing 100 ms autocomplete debounce is outside the measured boundary. A 35 ms callback-and-refresh result does not establish a universal 16.7 ms frame budget; the largest low-overhead candidate heartbeat observation in this completion series still exceeds 50 ms.

Validation includes a deterministic operation-count regression that fails on repeated catalog rescanning, namespace and case checks, and the existing completion suite. The initial focused lane passed 347 tests. The formal engine samples retain hashes of their suggestion lists so that speed and observed output can be compared together. Further dropdown reuse or batched mounting is a separate opportunity, with no speedup claimed here.

4. Startup: remove unnecessary work before promising faster launch

Worker preference fix implemented

The CLI creates its process worker before constructing the Textual app, which preserves an existing platform safeguard around process spawning. Previously, the saved process_worker=false setting was applied only during mount, after that worker had already been created. The fix reads this preference before prewarming. Enabled configurations retain the early spawn path.

image/svg+xml sqlit performance laboratory Default baseline Default implemented Disabled baseline Disabled implemented Empty .pyc implemented 0 200 400 600 800 1000 1200 Launch to observed first refresh (ms) Cold processes; warm filesystem cache Default baseline Default implemented Disabled baseline Disabled implemented 0 100 200 300 400 500 Launch-and-exit process CPU (ms) Respecting the disabled-worker setting
Figure 2. Startup wall time and consumed CPU. Normal conditions have 15 trials per source; empty bytecode has seven. Native timing still runs in these trials. CPU covers the first-refresh-and-exit process lifecycle, including reaped child work. Download SVG

With the saved worker disabled, launch-and-exit CPU falls from 541.8 to 454.4 ms, a 16.1% reduction with a 95% interval of about 14.9–19.6%. First-refresh medians move from 410.7 to 393.0 ms, but the interval crosses zero. Default-policy startup likewise has no clearly established improvement. The supported claim is less unnecessary CPU when the worker is disabled.

Import diagnostics place a large share of startup before the first app construction, including Textual, Rich, the results widget and Arrow. Import timings are inclusive and must not be added as though every module were independent. The separate empty-bytecode condition reaches first refresh at approximately 1,194 ms, versus 393 ms for the compiled-cache disabled-worker condition. This supports checking bytecode generation in source-only or unusual packaging paths; it does not show that a normal installer is missing that optimization.

The next architectural startup experiment should defer the heavy results backend behind a lightweight empty-results state. It requires preserving focus, table selectors, commands and the first-query experience. The observed import budget is an opportunity ceiling, not a measured saving from an implementation. This study does not recommend deferring security checks or disabling the enabled worker’s platform safeguards merely to improve a launch number.

5. Idle CPU: schedule work when it exists

Implemented and tested

The baseline idle scheduler arranges another check every 150 ms even with an empty queue. The change leaves no check scheduled when there is no work, arms a timer when a job arrives, disarms it after cancellation or pause, and resumes pending work correctly. Elapsed-time decisions use a monotonic clock. Reentrant job requests and stop/start behavior are covered by lifecycle tests.

image/svg+xml sqlit performance laboratory Baseline Implemented 0 10 20 30 40 50 60 Process CPU over 5 seconds (ms) 21.1 ms 5.9 ms Explorer focus Baseline Implemented 0 10 20 30 40 50 60 Process CPU over 5 seconds (ms) 39.9 ms 23.7 ms Editor focus; cursor blinking retained
Figure 3. Empty-queue polling costs measurable CPU even when the UI is still. Nine trials per condition, no 5 ms heartbeat during idle windows. The requested final refresh is included in both versions. Download SVG

With explorer focus, the five-second median CPU total falls from 21.1 to 5.9 ms. That is approximately 0.42% to 0.12% of one CPU core, or about 15 ms less consumed CPU per five seconds. The relative reduction is 72.0%, but the absolute magnitude is important. Editor-focus CPU falls from 39.9 to 23.7 ms while retaining cursor blinking.

This is a reduction in periodic application work, not a measured battery-life gain. Other wakeups, the terminal emulator and the rest of the desktop remain outside the process measurement. A cursor-blink-disabled pilot showed an additional preference-dependent opportunity, but was not adopted or promoted to a replicated claim. Keeping the existing visual behavior while eliminating empty polling is the better-supported change.

6. Rendering: throughput, stutter, memory and terminal traffic

The stock result path renders a small preview, then appends rows through idle work. The main comparison loads 50,000 six-column rows, while other fixtures exercise small results, 40-column tables, long text, Decimal values, scrolling and filtering. Full result availability, initial result refresh and event-loop lateness are retained separately. Original row counts and selected values are asserted.

image/svg+xml sqlit performance laboratory 1 0 2 1 0 3 Time until all 50,000 rows are available (ms) 20 30 40 50 60 70 80 90 Median of per-trial maximum loop lateness (ms) 16.7 ms excess-delay reference Rendering trade-offs; 50,000 × 6 cells; n = 5 Baseline (306 CPU ms) Implemented changes (312 CPU ms) Bulk, UI thread (141 CPU ms) Preview + thread (204 CPU ms) 500 rows / timer (418 CPU ms) Sampled row backend (62 CPU ms)
Figure 4. Faster completion can create worse stutter. Marker area scales with CPU, and horizontal error bars show median time uncertainty. Lateness is excess beyond a 5 ms sleep; the dashed line is a diagnostic reference, not proof of physical 60 FPS. Download SVG

Bulk rendering

Rejected as a general default

Building the whole table immediately reduces median full availability from 1,304.1 to 154.4 ms and reduces CPU by 54.1%. However, the median of each trial’s maximum heartbeat lateness rises from 22.1 to 94.7 ms. Finishing the batch sooner comes with a substantially longer single interruption.

Preview plus threaded preparation

Did not meet the responsiveness objective

The hybrid keeps an immediate preview, prepares a complete Arrow backend in a thread and replaces the table once. Full availability improves to 228.1 ms, but maximum lateness remains about 94.8 ms. The experiment demonstrates that moving preparation to a thread does not, by itself, remove the complete pipeline’s blocking behavior. Native conversion, GIL behavior and final mounting require separate attribution before selecting a more complex worker design. Python documents the relevant limitation of asyncio.to_thread.

More frequent timed batches

A measurable trade-off

Appending 500 rows per short timer reduces full availability to 524.7 ms while keeping the median maximum lateness near 21.2 ms. It also generates substantially more terminal output: 236,866.0 bytes versus 146,513.0. Its CPU point estimate increases. A foreground render scheduler therefore needs both a latency budget and redraw coalescing; shortening timers alone is not a lightweight solution.

Rows retained in Python, display work on demand

Strong prototype; incomplete production contract

The read-only prototype retains the existing Python rows and supplies visible values on demand, with widths sampled from 128 leading rows and the last row. It avoids converting the complete dataset into a second representation solely for display. Full availability falls to 67.1 ms and CPU to 62.2 ms: 94.9% and 79.7% reductions respectively in this fixture.

The prototype changes the column-width policy and does not implement all mutation/export contracts. Filtering and restoration still exercise stock application paths. It runs inside the existing Arrow-dependent app, so it does not demonstrate removal of that dependency or its import cost. The next development step is an explicit backend contract covering sort, copy, export, duplicate labels, mixed types, UUIDs, binary values, dates, decimals and cancellation. The measured gain justifies that work; it does not justify silently replacing the current backend.

image/svg+xml sqlit performance laboratory 0 50 100 150 200 250 Terminal payload (kB, decimal) Baseline Implemented changes Bulk, UI thread Preview + thread 500 rows / timer Sampled row backend Result-loading output volume 0 50 100 150 200 Process high-water RSS (MiB) Baseline Implemented changes Bulk, UI thread Preview + thread 500 rows / timer Sampled row backend Memory by this point in the fixed sequence
Figure 5. Terminal bytes and cumulative peak RSS at the 50,000-row workload. RSS includes imports and earlier workloads; it is not an allocation measurement for this query. The sampled backend still runs inside the existing Arrow-dependent application. Download SVG

Terminal bytes are the UTF-8 payload submitted to the terminal driver, excluding SSH framing, encryption, compression and retransmission. Scrolling still produces substantial output across backend strategies, so faster data preparation alone does not solve terminal transport cost. RSS is the process high-water value at a point in the same ordered workload sequence; it includes imports and earlier allocations and should not be read as the memory allocated by one operation.

Long-cell display bounds

Implemented; end-to-end speedup not established

The table’s formatter now honors the available width for oversized, single-line literal strings, using Rich’s cell-aware truncation. The backend value remains complete. Tests cover long ASCII, CJK, combining marks, emoji, markup-looking text and the existing styled-markup path. This gives the display representation a defined bound without truncating data used by the value viewer or other operations.

image/svg+xml sqlit performance laboratory Baseline Implemented 0 20 40 60 80 100 Milliseconds Consumed CPU Baseline Implemented 0 50 100 150 200 250 300 350 Milliseconds Full result availability
Figure 6. Isolated long-cell workload: 1,000 rows, three columns, 10,000-character text, five fresh processes per source. The wide uncertainty prevents a reliable end-to-end speedup claim for the clipping change. Download SVG

The repeated fresh-process workload does not establish a reliable whole-operation speedup: CPU medians are 90.6 and 86.3 ms, with a reduction interval spanning roughly −20% to +30%. Full-availability timing is similarly uncertain. The earlier pilot looked stronger, illustrating why the paper uses the repeated study for conclusions. This change should receive normal visual review, with no broad performance credit assigned from these timings.

A separate headless visual fixture shows literal markup and Unicode values at their display bounds, with all 48 original values checked in the backend. Its rows are injected for display verification; its displayed query time is not a database measurement.

Detailed rendering effect estimates
ComparisonBeforeAfterSavedReduction95% intervalnReading
Bulk, UI thread · availability (ms)1,304.1154.41,149.788.2%84.7 to 91.5%5/5Observed reduction
Bulk, UI thread · CPU (ms)305.8140.5165.354.1%45.3 to 67.3%5/5Observed reduction
Bulk, UI thread · maximum loop lateness (ms)22.194.7-72.6-327.6%-435.4 to -93.1%5/5Observed increase
Bulk, UI thread · terminal bytes (bytes)146,513.077,109.069,404.047.4%46.4 to 50.8%5/5Observed reduction
Preview + thread · availability (ms)1,304.1228.11,076.082.5%78.0 to 88.3%5/5Observed reduction
Preview + thread · CPU (ms)305.8203.6102.233.4%16.2 to 55.0%5/5Observed reduction
Preview + thread · maximum loop lateness (ms)22.194.8-72.6-328.0%-575.6 to -76.7%5/5Observed increase
Preview + thread · terminal bytes (bytes)146,513.097,519.048,994.033.4%32.2 to 37.7%5/5Observed reduction
500 rows / timer · availability (ms)1,304.1524.7779.459.8%46.9 to 70.4%5/5Observed reduction
500 rows / timer · CPU (ms)305.8417.8-112.0-36.6%-82.6 to 9.8%5/5Interval crosses zero
500 rows / timer · maximum loop lateness (ms)22.121.20.94.1%-58.9 to 60.7%5/5Interval crosses zero
500 rows / timer · terminal bytes (bytes)146,513.0236,866.0-90,353.0-61.7%-82.6 to -37.2%5/5Observed increase
Sampled row backend · availability (ms)1,304.167.11,237.094.9%93.8 to 95.6%5/5Observed reduction
Sampled row backend · CPU (ms)305.862.2243.679.7%77.2 to 84.1%5/5Observed reduction
Sampled row backend · maximum loop lateness (ms)22.119.32.912.9%-1.4 to 60.4%5/5Interval crosses zero
Sampled row backend · terminal bytes (bytes)146,513.077,109.069,404.047.4%46.4 to 50.8%5/5Observed reduction

7. Database transport: bound work where it happens

Each server contains a 50,000-row fixture with an integer key and 256-character payload. Tests use the real sqlit adapter, then compare a client-side 1,000-row cap, an explicit LIMIT 1001 plus that cap, and a provider-specific streaming cursor. A separate relay adds either zero or 20 ms delivery delay in each direction, while counting transmitted protocol payload. It pipelines data instead of sleeping once per SQL call or serially throttling every chunk.

image/svg+xml sqlit performance laboratory PostgreSQL MySQL MariaDB 0 2 4 6 8 10 12 14 Received protocol payload (MB, decimal) A Python row cap is not a network cap Buffered cap: 1,000 Explicit LIMIT 1,001 Streaming + cleanup PostgreSQL MySQL MariaDB 0 50 100 150 200 250 Operation wall time (ms) 40 ms injected round-trip latency
Figure 7. All bounded result variants verify rows 1–1,000 and truncation. Streaming includes cursor cleanup and a subsequent SELECT 1. PostgreSQL named cursors avoid full transfer; MySQL/MariaDB SSCursor cleanup still consumes the remaining result. Nine trials per condition. Download SVG

A 1,000-row cap still receives approximately 13.79 MB in PostgreSQL and 13.44 MB in MySQL and MariaDB for the unbounded SQL fixture. The explicitly limited query receives approximately 274 kB and 267 kB respectively, a roughly 98% reduction. All bounded variants verify the same first 1,000 rows and a true truncation indication. With 40 ms injected round-trip latency, the bounded query also removes substantial CPU and elapsed work.

This result concerns arbitrary unbounded SQL followed by a Python fetch cap. Generated table-preview queries already use dialect-specific SQL limits. A generic textual rewrite of every submitted statement would be unsafe for syntax and semantics: multi-statement batches, existing LIMIT clauses, locking reads, side-effecting functions and DML-returned rows need their own handling. The immediate recommendation is a clear bounded-preview execution contract, not an unconditional rewrite of user SQL.

Streaming is provider-specific

PostgreSQL’s regular cursor ordinarily transfers the complete result, while a named server-side cursor supports controlled fetching. The named-cursor experiment uses a transaction, closes the cursor, rolls back and restores the connection before a health query. These lifecycle requirements are part of the approach, as described in the Psycopg documentation.

PyMySQL’s SSCursor returns initial rows without buffering everything, but its close operation exhausts the unread result. The lab measures this cleanup and confirms that the connection can execute SELECT 1 afterward. The full payload is still transferred for MySQL/MariaDB. This matches the documented cursor behavior. Streaming timings in the figure include that extra health query, so their boundary includes more work than the ordinary limited-query column.

Bandwidth figures count relay payload, including protocol messages; they exclude TCP/IP headers and packet retransmissions. The byte intervals collapse for this deterministic fixture because repeated executions produce the same payload size. That does not imply identical savings for arbitrary schemas or real cloud services.

8. Metadata and connection round trips

The existing PostgreSQL and MySQL-family column-inspection paths query primary-key information and column information separately. Scanning 20 fixture tables therefore requires 40 metadata queries. The lab compares that path with one query per table and a single query returning all 80 columns. Ordering, type names and primary-key flags must match the existing adapter results.

image/svg+xml sqlit performance laboratory PostgreSQL MySQL MariaDB 0 20 40 60 80 100 20-table metadata time (ms) Loopback relay, no added delay Two queries / table One query / table One batch / 20 tables PostgreSQL MySQL MariaDB 0 250 500 750 1000 1250 1500 1750 20-table metadata time (ms) 40 ms injected round-trip latency
Figure 8. Reducing metadata round trips dominates at latency. Every strategy returns the same 80 ordered columns and primary-key flags for 20 owned fixture tables. These are explicit catalog-scan experiments, not measurements of the default lazy connection flow. Download SVG

At 40 ms injected round-trip latency, the 20-table scan takes about 1.66–1.72 seconds on the baseline paths. One query per table removes approximately half the latency; a single batch reduces the scan to about 44–52 ms, approximately 97% faster. Loopback improvements are smaller and more dependent on query planning. This is strong evidence for reducing round trips where multiple metadata requests are genuinely required.

The default application loads much metadata lazily, so the experiment is not evidence that every connection currently performs this 20-table scan. A provider-specific bulk metadata capability should be invoked by actual demand and preserve schema/database identity, permissions, ordering and cache invalidation. It should not eagerly fetch an entire organization’s catalog simply because one batch can do so efficiently.

image/svg+xml sqlit performance laboratory PostgreSQL MySQL MariaDB 0 1 2 3 4 5 6 7 8 SELECT 1 operation (ms) Loopback relay, no added delay Fresh connection + query Reused connection + query PostgreSQL MySQL MariaDB 0 50 100 150 200 SELECT 1 operation (ms) 40 ms injected round-trip latency
Figure 9. Measured connection reuse opportunity. These connections use local disposable credentials and no TLS. The current cancellable query path deliberately creates dedicated connections; safe pooling must preserve cancellation, transaction and session-state boundaries. Download SVG

For a short query at the injected latency, retaining a connection reduces median operation time from approximately 170 to 41 ms in PostgreSQL, 186 to 42 ms in MySQL, and 227 to 41 ms in MariaDB. The current cancellable-query implementation intentionally creates a dedicated connection, allowing cancellation to close it. This makes connection establishment a real opportunity and a real lifecycle constraint.

A useful next experiment is an exclusive-lease connection pool that discards cancelled or uncertain connections and explicitly resets reusable session state. Reset work may add round trips and reduce the available gain. Transactions, temporary objects, session settings, credential changes and cross-database routing must be validated before integration. The numbers above measure retained-connection opportunity; they are not measurements of a completed safe pool.

Detailed network latency effect estimates

All before/after values below are milliseconds. The final suffix “20” denotes 20 ms added in each direction.

ComparisonBeforeAfterSavedReduction95% intervalnReading
Postgresql · bounded query (ms)110.642.368.361.7%61.5 to 61.9%9/9Observed reduction
Postgresql · one query / table (ms)1,724.7924.8799.946.4%46.1 to 46.9%9/9Observed reduction
Postgresql · metadata batch (ms)1,724.751.61,673.197.0%96.7 to 97.1%9/9Observed reduction
Postgresql · connection reuse (ms)170.141.0129.075.9%75.7 to 76.3%9/9Observed reduction
Mysql · bounded query (ms)163.244.2119.072.9%72.2 to 74.2%9/9Observed reduction
Mysql · one query / table (ms)1,673.0837.7835.349.9%49.7 to 50.1%9/9Observed reduction
Mysql · metadata batch (ms)1,673.044.21,628.997.4%97.3 to 97.5%9/9Observed reduction
Mysql · connection reuse (ms)185.541.5144.077.6%76.5 to 78.0%9/9Observed reduction
Mariadb · bounded query (ms)156.044.3111.671.6%68.3 to 72.5%9/9Observed reduction
Mariadb · one query / table (ms)1,660.9833.7827.249.8%49.5 to 50.2%9/9Observed reduction
Mariadb · metadata batch (ms)1,660.943.71,617.297.4%97.3 to 97.5%9/9Observed reduction
Mariadb · connection reuse (ms)226.641.2185.481.8%81.6 to 81.9%9/9Observed reduction

9. Local databases, process isolation and IPC

SQLite and DuckDB experiments compare reused connections, fresh connections, the application’s cancellable-query object and the actual process-worker client. They also compare 1,000-row and 50,000-row transfers. Fixtures live in temporary files. DuckDB uses read-only connections so that the forced cross-process experiment does not depend on unsupported concurrent writers.

image/svg+xml sqlit performance laboratory 1 0 2 1 0 1 1 0 0 1 0 1 1 0 2 SELECT 1 operation (ms, log scale) Reuse Connect / query / close Cancellable query Warm worker Cold worker / shutdown sqlite 1 0 0 1 0 1 1 0 2 SELECT 1 operation (ms, log scale) Reuse Connect / query / close Cancellable query Warm worker Cold worker / shutdown duckdb
Figure 10. Embedded database connections and actual process-worker costs; nine trials each. Cold worker includes creation, execution and shutdown. DuckDB worker runs are forced laboratory comparisons with read-only files; the adapter disables that route in the normal UI. Download SVG

Process creation and communication can dominate a trivial local query. A warm worker avoids repeated interpreter initialization, but the current worker still creates a dedicated query connection. Parent CPU measurements exclude the worker’s CPU; the worker’s reported operation time is retained separately. The cold-worker condition includes startup, execution and shutdown. In normal UI use, DuckDB advertises that the process-worker route is unsupported, so its forced comparison is an architectural experiment rather than a measurement of that adapter’s default behavior.

These results favor reusing an already-needed worker and avoiding an unwanted worker, while retaining cancellation and crash isolation where required. They do not support globally removing isolation. For long queries, the startup cost may be small relative to execution; for a rapid sequence of local previews, it can be the dominant term.

image/svg+xml sqlit performance laboratory 0 10 20 30 40 Serialization + read-back (ms) Pickle: Python → Python Arrow: Python → Python Arrow: prepared → prepared Arrow LZ4: prepared Arrow Zstd: prepared 50,000 rows; repeated 256-character text 0 10 20 30 40 Serialization + read-back (ms) Pickle: Python → Python Arrow: Python → Python Arrow: prepared → prepared Arrow LZ4: prepared Arrow Zstd: prepared 50,000 rows; varied 256-character text
Figure 11. Representation boundaries determine the result. The first two rows include Python-to-Python round trips; the last three start and end as Arrow tables and exclude conversion. Each decoded result is checked for equality. Repeated strings are independent objects, avoiding artificial pickle memoization. Download SVG

The serialization experiment intentionally distinguishes two contracts. A Python-row round trip includes serialization and reconstruction of Python values. A prepared-Arrow round trip starts and ends with a columnar table. Arrow’s buffer-sharing and IPC properties can be valuable in a pipeline that stays columnar, but they do not eliminate the conversions required by an otherwise row-oriented pipeline. The full Python-to-Arrow-to-Python variant is measured separately. The Arrow IPC documentation describes the underlying format and zero-copy opportunities.

Compression is data-dependent. Both repeated text and varied deterministic hexadecimal text are exercised. Repeated strings are allocated independently to match driver-returned values, preventing pickle from receiving an artificial advantage through object-identity memoization. LZ4 and Zstd results are available with byte sizes and round-trip times. A blanket serializer or compressor replacement is not supported; a sustained columnar path needs its own end-to-end experiment.

10. Recommended actions

The highest-confidence changes are small and localized. The larger opportunities are supported by actual prototypes or real-server comparisons, but retain explicit implementation gates. Savings from unrelated rows in this table must not be added into an “overall percentage.”

Priority and statusActionMeasured evidence and next gate
P0
Implemented
Index routine identities once per completion request.5,000-routine app completion: 1,841.2 → 34.6 ms. Preserve the operation-count and namespace regressions.
P1
Implemented
Stop empty idle-queue polling.Explorer-focus idle CPU: 21.1 → 5.9 ms per five seconds. Verify pause, resume, cancellation and new work after drain.
P1
Implemented
Honor the saved disabled-worker setting before prewarm.Launch-and-exit CPU decreases 16.1%. Keep the enabled worker’s early-spawn safeguard; no default startup speedup is established.
P1
Bounded display; timing uncertain
Keep long literal display values within the available cell width.Data fidelity and Unicode tests pass. Repeated whole-operation CPU and wall-time intervals cross zero; assign no proven overall saving.
P2
Real-server prototype
Provide one-query and demand-driven bulk metadata APIs.20-table scans at 40 ms added RTT improve by about 50% and 97%. Validate non-default schemas, duplicate names, visibility and invalidation.
P2
Real-server evidence
Make bounded retrieval an explicit execution path.Approximately 98% less response payload for the fixture. Preserve arbitrary SQL semantics; do not equate a Python row cap with a server limit.
P2
Opportunity measured
Test exclusive connection reuse with cancellation and reset.Short-query opportunity of roughly 76–82% at injected latency. Measure the actual reset/discard pool rather than promising the retained-connection upper bound.
P2
Rendering prototype
Develop a row-backed or sustained-columnar result pipeline.Sampled row backend: 50,000-row availability 1,304.1 → 67.1 ms; CPU 79.7% lower. Complete the backend feature contract first.
P3
Follow-up design
Coalesce redraws and give foreground rendering a bounded work budget.The timer experiment finishes sooner but increases terminal bytes by roughly 62%. Include active typing, resize, cancellation and slow-terminal tests.
P3
Budget identified
Audit bytecode packaging and defer heavy empty-result imports.Empty-bytecode startup is materially slower. Deferred imports still need a real first-query/focus prototype; no unbuilt speedup is claimed.

Approaches to avoid adopting from these results

Do not replace incremental rendering with a synchronous bulk build solely because completion time improves. Do not assume a thread removes native or mounting stalls. Do not switch all MySQL reads to SSCursor expecting a bandwidth cap. Do not remove cancellation isolation to reproduce a microbenchmark. Do not choose an IPC format using a comparison that excludes its required conversions. A width-cache intervention and disabling cursor blink were explored in pilots, but did not supply the same quality of evidence as the implemented fixes.

11. Validation and limits

All 152 formal jobs completed. The study asserts fixture result counts, selected cell values, truncation, metadata equivalence, successful post-stream connection use and serialization equality. Each owned Docker server was stopped and removed; cleanup logs retain exit and OOM state. Dedicated diagnostics provide the startup/import logs, native watchdog output, structured events, cProfile output and an independent sampling profile.

1,801 tests passed; 14 skipped; 0 failed. The broader lane ran the unit, UI, CLI, SQLite and DuckDB suites. The focused lanes are subsets and are not added to this total.

Skip reasons: could not import 'databricks.sdk.core': No module named 'databricks' (1); could not import 'oracledb': No module named 'oracledb' (2); SQLite is file-based, no Docker container (3); SQLite does not support sequences (2); SQLite does not have a timezone-aware datetime type (1); DuckDB is file-based, no Docker container (3); DuckDB does not support triggers (2). These skips do not stand in for live provider verification. Existing CLI lint findings were checked against the baseline; the touched production files introduce no new lint category in that comparison. Download validation details.

A separate Python 3.10 compatibility lane passed 408 targeted tests, with 0 failures and 0 skips. These tests overlap the main lane and are reported separately.

The primary before/after estimates are below. Values are milliseconds, including CPU milliseconds where named. An interval crossing zero means that this sample does not establish the direction of the change. The saved-worker condition and the default-worker condition are intentionally separate.

ComparisonBeforeAfterSavedReduction95% intervalnReading
App completion · 1000 routines (ms)98.421.876.677.9%65.9 to 79.9%9/9Observed reduction
App completion · 5000 routines (ms)1,841.234.61,806.698.1%97.0 to 98.2%9/9Observed reduction
First refresh · worker disabled (ms)410.7393.017.74.3%-0.1 to 7.4%15/15Interval crosses zero
Launch CPU · worker disabled (ms)541.8454.487.416.1%14.9 to 19.6%15/15Observed reduction
First refresh · worker default (ms)409.0402.46.61.6%-5.1 to 4.5%15/15Interval crosses zero
Launch CPU · worker default (ms)547.4537.59.91.8%-4.3 to 3.8%15/15Interval crosses zero
Idle CPU · explorer (ms)21.15.915.272.0%61.5 to 74.0%9/9Observed reduction
Idle CPU · editor (ms)39.923.716.140.5%29.2 to 47.7%9/9Observed reduction
Long cells · CPU (ms)90.686.34.34.8%-20.1 to 30.2%5/5Interval crosses zero
Long cells · availability (ms)181.4189.9-8.5-4.7%-14.5 to 42.2%5/5Interval crosses zero

External validity

This is one Linux laptop and one locked software environment. CPU frequency, thermal state and background activity were not pinned; load averages and environment metadata are retained. No physical terminal-emulator GPU timing, monitor presentation trace, battery energy or network packet loss was measured. Therefore, “60 FPS,” battery-life percentages and universal startup claims would go beyond the evidence.

The network-delay relay models fixed one-way latency, not a complete mobile or WAN network. It adds no loss, jitter, constrained bandwidth or TLS handshake, and its local database servers use small synthetic schemas. Paid cloud cold starts, OAuth/token refresh, SSH tunnels, enterprise catalog permissions, Oracle, SQL Server, Databricks, Exasol, Snowflake and other providers are outside the measured server matrix. No existing private connection was used to fill those gaps.

The principal UI dataset order is fixed, so later RSS observations include earlier allocations. Synthetic values cannot represent every driver object or LOB lifecycle. Repeated UI completion invokes the real application method and dropdown with supplied SQL text; it does not include physical keystroke transport or the existing debounce delay. Continuous typing during large-result ingestion, aggressive resize and all production cancellation races remain follow-up workloads.

Only the measured source changes are implemented in the branch. Experimental variants remain laboratory code. The study does not establish an installed release, a merge to main or a deployment. The baseline and measured candidate commits remain the reproducible references even if main changes later.

12. Explore the measurements

This table is generated from the recorded samples. Select an experiment family and metric, filter by provider, variant or scenario, and download the current view. Confidence intervals describe the median. The full JSON also retains means, standard deviations and quartiles.

ScenarioVariant / engineConditionnMedian95% median intervalp95MinimumMaximum
All raw observation files

Lossless gzip-compressed JSON chunks include individual heartbeat samples and frame-call timestamps where available. The manifest supplies SHA-256 hashes. These are normalized observations; local run logs and the selected diagnostic files have separate provenance.

13. Reproduction and diagnostic tools

The lab scripts are stored in labs/performance/. Create separate worktrees at the referenced baseline and candidate commits and install the frozen dependencies. The controller needs only the standard library; plotting uses a separate Matplotlib environment so that plotting dependencies cannot change the measured app’s import graph.

uv sync --frozen --group dev --extra postgres --extra mysql --extra duckdb

python labs/performance/study.py \
  --baseline /absolute/baseline \
  --candidate /absolute/candidate \
  --python /absolute/candidate/.venv/bin/python \
  --output /absolute/evidence \
  --docker --dry-run

# Remove --dry-run to execute. --quick is a smoke run only.
# Without Docker: --phases startup,cpu,completion,render,idle

python labs/performance/summarize.py \
  --input /absolute/evidence --output docs/performance

# In a separate environment with Matplotlib:
python labs/performance/build_report.py --data docs/performance/data.json

The network helper only accepts disposable local containers. Individual probes can enable --diagnostics --profile, and output both Chrome trace JSON and the native debug/watchdog logs. py-spy record --format speedscope supplies a complementary sampling profile. Download the selected diagnostic recordings and regression logs. Tests should use isolated sqlit configuration; the old personal-connection watchdog integration test is not a substitute for a disposable fixture.

Offline trace viewer

Open a generated ui.trace.json file to inspect workload spans and heartbeat lateness locally. No file is uploaded. This viewer is a companion diagnostic tool; it does not convert application display calls into physical FPS.

Choose a generated trace file to display its measurements.

The lab’s trace uses logical lanes: workload spans and heartbeat observations.

References and evidence

  1. sqlit source. Baseline 9db49c230c7c and measured candidate 0640c96464dc. Key source areas: completion engine; CLI prewarm; idle scheduler; result formatting; cursor adapters; cancellable queries and process worker.
  2. Study data. 1349 normalized observations, descriptive statistics and effect intervals, lossless raw chunks and hashes, regression validation, and source/data provenance. The measurement-file hashes match laboratory snapshot 03e68b73999e.
  3. Python Software Foundation. Time access and conversions, Python 3.13 documentation. Clock definitions and process CPU semantics. Accessed 10 September 2026.
  4. Python Software Foundation. Coroutines and tasks: asyncio.to_thread. Threading and GIL limitations. Accessed 10 September 2026.
  5. Textualize. Textual workers. Background work and UI interaction contracts. Installed Textual version measured: 8.2.8.
  6. Psycopg project. Server-side cursors and transaction lifecycle. Installed psycopg2-binary version: 2.9.11. Accessed 10 September 2026.
  7. PyMySQL project. Cursor objects: SSCursor and close. Installed PyMySQL version: 1.1.2. Accessed 10 September 2026.
  8. Apache Arrow. Streaming, serialization and IPC and concat_tables. The installed implementation measured here is PyArrow 21.0.0; documentation describes the representation and buffer-sharing contracts.

This HTML embeds its figures and statistical explorer and requires no external JavaScript, font or image service. The SVG figures, CSV, statistics JSON and raw chunks remain separate downloadable artifacts. Measured outcomes, experimental interventions and proposed follow-up work are intentionally identified throughout.