9.5 KiB
Table Rendering Memory Optimization Plan
Executive Summary
This plan outlines improvements to eliminate excessive memory allocations and copies in the Odin table rendering system. The current implementation makes 10+ allocations per row, while the Zig equivalent makes zero allocations for rendering. This optimization will reduce memory usage, improve performance, and align with the project's efficiency goals.
Current State Analysis
Zig Version (Reference Implementation)
- Allocations: 1 (data only)
- Data copies: 0
- String allocation: 0
- Column widths: Stack array
- Output: Direct to writer
Odin Version (Current Implementation)
- Allocations: 10+ per row
- Data copies: Multiple per row
- String allocation: 2+ per row (concatenate + slice)
- Column widths: Heap allocated
- Output: Builder → stdout
Current Issues Identified
-
Table Infrastructure (
table.odin)- Uses
strings.Builderwhich allocates per-line memory - Heap-allocated
[dynamic]intfor column widths - Multiple
strings.concatenate()calls creating new strings
- Uses
-
Command Implementations
cmd_list: Creates intermediate[]stringslices per row, allocates new strings viastrings.concatenate()cmd_sync: CreatesSyncEntrystructs with cloned strings, allocates dynamic arrayscmd_deps: Allocates dynamic rows array unnecessarily
-
Memory Pattern
- Each command allocates
[][]stringfor table data - Manual struct-to-row transformation creates copies
- Duplicate code across all table-using commands
- Each command allocates
Proposed Solutions
Phase 1: Core Table Infrastructure Overhaul
1.1 Direct Writer-Based Rendering
Current:
b: strings.Builder
strings.builder_init(&b)
// ... build table in builder
fmt.println(strings.to_string(b))
Proposed:
render_table :: proc(writer: io.Writer, headers: []string, rows: [][]string)
- Replace
strings.Builderwithio.Writeroutput - Eliminate intermediate string allocations
- Write table components directly to output stream
1.2 Stack-Based Column Widths
Current:
col_widths := make([dynamic]int, 0, len(headers))
Proposed:
- Use fixed stack arrays for reasonable column counts
- Implement small buffer optimization (SBO) for variable column counts
- Only allocate for tables exceeding threshold (e.g., 16 columns)
1.3 Zero-Copy String Handling
Current:
dir_str := strings.concatenate({row.Dir, "/"}, context.temp_allocator)
Proposed:
- Replace
strings.concatenate()with string slicing - Work directly with
EnvFile.PathandEnvFile.Dirfields - Use
filepath.base()andfilepath.dir()without allocation where possible
Phase 2: Generic Table Interface
2.1 Field-Based Table Renderer
Table_Field :: struct {
name: string,
value: string, // String view, no allocation
alignment: Alignment,
}
Table_Config :: struct {
writer: io.Writer,
fields: []Table_Field,
col_widths: []int,
}
render_row :: proc(cfg: Table_Config, row_data: any)
- Accept struct fields directly without intermediate arrays
- Support field selection (show only specific fields)
- Alignment options (left/center/right)
2.2 Field Extraction Procs
- Generate field extraction helpers for each struct type
- Avoid string allocation by returning string views
- Cache computed values (like formatted status strings)
2.3 Streaming Table Processing
- Process rows one at a time without collecting all rows
- Reduce peak memory usage from O(N × strings) to O(table_structure)
- Enable early termination if needed
Phase 3: Command-Specific Optimizations
3.1 Eliminate Intermediate Structs
Current (cmd_sync):
for &file in files {
// ... processing
path_str, _ := strings.clone(file.Path)
status_str, _ := strings.clone(status)
append(&results, SyncEntry{Path = path_str, Status = status_str})
}
Proposed:
for &file in files {
result, err_msg := db_sync(&db, &file)
// Direct rendering with zero-copy
render_sync_row(writer, file, result, err_msg)
}
cmd_sync: Work directly withEnvFile+SyncFlagEnumcmd_list: UseEnvFilefields directly, noListEntry- Generate table content on-the-fly
3.2 In-Place Status Computation
get_sync_status :: proc(result: SyncFlag, err_msg: string) -> string {
switch {
case .Error in result: return if len(err_msg) > 0 then err_msg else "error"
case .BackedUp in result: return "Backed Up"
case .Restored in result: return "Restored"
case .DirUpdated in result: return "Moved"
case: return "OK"
}
}
- Compute status strings without allocation (use static lookup)
- Cache formatted status values if needed
- Reduce allocation count from N to 0 or 1
3.3 Batch Processing
- Reduce allocation count by pooling small allocations
- Use
context.temp_allocatormore effectively - Pre-allocate buffers for expected sizes
Phase 4: JSON Output Separation
4.1 Unified JSON Rendering
render_json_rows :: proc(writer: io.Writer, rows: any, field_names: []string)
- Create centralized JSON rendering helper
- Work with same structs as table rendering
- Use reflection or explicit field marshaling
4.2 Format-Agnostic Interface
- Commands generate data → renderers handle format
- Table renderer focuses only on ASCII/Unicode output
- Keep terminal detection in command layer
Expected Improvements
| Metric | Current | Target | Improvement |
|---|---|---|---|
| Allocations | 10+ per row | 0-1 per table | 10x+ reduction |
| Memory copies | 2-3 per row | 0 | 100% reduction |
| Peak memory | O(N × strings) | O(table_structure) | Constant factor |
| Throughput | Baseline | 2-3x faster | Performance boost |
Implementation Strategy
High-Priority Changes
- Replace
strings.Builderwith directio.Writeroutput - Convert column widths to stack-based allocation
- Eliminate intermediate struct allocations in commands
Medium-Priority Changes
- Create generic field-based table interface
- Implement streaming table processing
- Centralize JSON rendering logic
Low-Priority Changes
- Add alignment options beyond left-aligned
- Implement comprehensive field introspection
- Add advanced table formatting features
Tradeoff Questions
Before implementation begins, we need to resolve these architectural questions:
1. Generality vs. Performance
Question: Should we create a fully generic table renderer (similar to Zig's Table(T)) or focus on optimizing the current 3 use cases first?
Options:
- Generic approach: Higher development cost, future-proof, may have some overhead
- Specific optimization: Faster implementation, maximum performance for current use cases, less flexible
Recommendation: Start with specific optimizations for current use cases, then generalize patterns that emerge.
2. Alignment Support
Question: Does the project need left/center/right alignment support, or is left-alignment sufficient?
Context: Zig supports alignment options, but current Odin implementation only left-aligns. Most CLI tables work fine with left alignment.
Recommendation: Start with left-alignment only, add alignment if specific use cases demand it.
3. API Compatibility
Question: Should we maintain the current render_table() API signature, or are breaking changes acceptable?
Current API:
render_table :: proc(headers: []string, rows: [][]string)
Options:
- Maintain API: Slower to implement, backward compatible, may need adapter layers
- Break API: Faster implementation, cleaner code, requires updates to all callers
Recommendation: Breaking changes are acceptable since this is an optimization-focused effort and callers are limited to 3 commands.
4. Odin Capabilities
Question: What runtime reflection or field introspection capabilities does Odin provide?
Context: Zig uses @typeInfo() and comptime field iteration. We need to understand Odin's equivalent capabilities to design the optimal solution.
Recommendation: Investigate Odin's runtime type information capabilities before finalizing the generic table interface design.
5. Testing Strategy
Question: Should we add comprehensive tests for new table rendering before optimizing commands, or optimize incrementally with tests added afterwards?
Options:
- Test-first: More robust, catches regressions early, slower initial development
- Optimize-first: Faster development, may miss edge cases, requires retroactive testing
Recommendation: Hybrid approach - add basic tests for core infrastructure, then optimize incrementally with additional tests for each command.
Next Steps
- Research Phase: Investigate Odin's type system and reflection capabilities
- Prototype Phase: Create minimal working prototype of zero-allocation table renderer
- Refactor Phase: Incrementally update commands to use new infrastructure
- Test Phase: Add comprehensive tests and verify memory improvements
- Benchmark Phase: Measure performance improvements and memory usage
Success Criteria
- Zero allocations for table rendering (excluding initial data)
- Zero string copies in the happy path
- All 3 commands (
list,sync,deps) use new infrastructure - Performance improvement of 2x or more
- Memory usage reduction of 50% or more
- No regression in table formatting quality
- Backward compatibility with JSON output format