Skip to content

perf: framebuffer renders O(chars) bash iterations + O(cells) printf calls per frame #39

@fissible

Description

@fissible

Problem

shellframe_fb_print loops character-by-character over every string it writes. For each character: one bash substring op (${_str:$_i:1}), one arithmetic expansion, one array write, one append to _SF_FRAME_DIRTY. shellframe_fb_fill has the same structure.

shellframe_screen_flush then emits one cursor-position printf per changed cell.

Cost in shellql grid rendering

A typical data grid: 50 visible rows × 10 columns × ~15 chars/cell average.

Phase Operations per frame
shellframe_fb_fill (row backgrounds) ~6,000 bash iterations
shellframe_fb_print (cell content) ~7,500 bash iterations
shellframe_screen_flush dirty scan + printf up to ~7,500 write() syscalls on full redraw

~21,000+ bash operations per render frame. On macOS bash 5 this is 50–200 ms/frame — slow enough to make grid scrolling feel laggy at typical key-repeat rates.

The data-loading side (sqlite3 queries) is not the bottleneck once data is in memory. The render loop is.

Root cause

The framebuffer stores one cell per array slot. This requires character-level decomposition on write and character-level cursor positioning on flush. Both are O(total_chars) in bash loops.

Proposed fix

Store strings per row, not per character

Replace _SF_FRAME_CURR[$char_idx] with _SF_ROW_CURR[$row] — an assembled ANSI string per row. On flush, each changed row emits as a single printf:

printf '\033[%d;1H\033[0m%s' "$_row" "${_SF_ROW_CURR[$_row]}" >&3

This reduces flush from O(chars) printf calls to O(changed_rows) — roughly 50× fewer syscalls for a full grid scroll.

Row-level dirty tracking

Mark dirty rows (_SF_DIRTY_ROWS[$row]=1) instead of individual cell indices. Diff _SF_ROW_CURR[$row] vs _SF_ROW_PREV[$row] as a whole string — one comparison per row instead of N per row.

Trade-offs

shellframe_fb_print needs to splice into a pre-built row buffer rather than writing isolated cells. shellframe_fb_print_ansi needs updating to track accumulated attributes across splices. A row with one changed cell rewrites the full row string — but terminal throughput for a 200-char string is far cheaper than 200 individual cursor-position escape sequences.

Impact

Affects all shellframe consumers; most visible in grid/list widgets with wide content. This is the change that makes smooth scrolling achievable in bash.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions