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.
Problem
shellframe_fb_printloops 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_fillhas the same structure.shellframe_screen_flushthen emits one cursor-positionprintfper changed cell.Cost in shellql grid rendering
A typical data grid: 50 visible rows × 10 columns × ~15 chars/cell average.
shellframe_fb_fill(row backgrounds)shellframe_fb_print(cell content)shellframe_screen_flushdirty scan + printf~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 singleprintf: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_printneeds to splice into a pre-built row buffer rather than writing isolated cells.shellframe_fb_print_ansineeds 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.