|
| 1 | +# Circular Buffers in Pharo: A Better Way to Handle Recent Data |
| 2 | + |
| 3 | +### The Problem: Managing Recent Data is Harder Than It Looks |
| 4 | + |
| 5 | +Every developer faces this challenge: keeping track of the last N items efficiently. Whether it's recent chat messages, browser history, or sensor readings, the naive approach quickly becomes problematic: |
| 6 | + |
| 7 | +```smalltalk |
| 8 | +"Current Approach" |
| 9 | +recentMessages := OrderedCollection new. |
| 10 | +recentMessages add: newMessage. |
| 11 | +recentMessages size > 100 ifTrue: [ |
| 12 | + recentMessages removeFirst "Ouch! This shifts 99 elements EVERY time" |
| 13 | +]. |
| 14 | +``` |
| 15 | + |
| 16 | +This innocent-looking code has serious flaws: |
| 17 | +- **Performance degradation**: Each `removeFirst` becomes slower as the collection grows |
| 18 | +- **Memory spikes**: Collection grows before cleanup |
| 19 | +- **Complexity**: Manual size management and error-prone logic |
| 20 | + |
| 21 | +### The Solution: Circular Buffers |
| 22 | + |
| 23 | +A Circular buffer is a fixed-size data structure that automatically overwrites the oldest data when it reaches capacity. Think of it as a smart parking lot with exactly N spaces - when full, new cars automatically replace the oldest parked cars. |
| 24 | + |
| 25 | +### Key Benefits |
| 26 | +- **Constant Memory Usage**: Never grows beyond specified capacity |
| 27 | +- **O(1) Performance**: Lightning-fast operations regardless of data volume |
| 28 | +- **Zero Memory Leaks**: Automatic cleanup of old data |
| 29 | +- **Streaming Optimized**: Designed for continuous data flow |
| 30 | +- **Flexible Ordering**: Choose FIFO or LIFO based on your needs |
| 31 | + |
| 32 | +```smalltalk |
| 33 | +"Circular buffer approach - elegant and efficient" |
| 34 | +recentMessages := CTFIFOBuffer withCapacity: 100. |
| 35 | +recentMessages push: newMessage. "That's it! Always O(1) & No Manual Cleanup" |
| 36 | +``` |
| 37 | + |
| 38 | +### Pharo provides two types of circular buffers: |
| 39 | +- **FIFO Buffer**: First In, First Out (Queue behavior) |
| 40 | +- **LIFO Buffer**: Last In, First Out (Stack behavior) |
| 41 | + |
| 42 | + |
| 43 | +### FIFO Buffer: Deep Dive with Example |
| 44 | + |
| 45 | +FIFO buffers work like a queue - the first element added is the first one retrieved. |
| 46 | + |
| 47 | +### Dry Run Example |
| 48 | +```smalltalk |
| 49 | +buffer := CTFIFOBuffer withCapacity: 2. |
| 50 | +buffer push: 'A'. "Buffer state: [A, _] readIndex=1, writeIndex=2" |
| 51 | +buffer push: 'B'. "Buffer state: [A, B] readIndex=1, writeIndex=1" |
| 52 | +buffer push: 'C'. "Buffer state: [C, B] readIndex=2, writeIndex=2 (A overwritten)" |
| 53 | +buffer pop. "Returns 'B', Buffer state: [C, _] readIndex=1, writeIndex=2" |
| 54 | +buffer pop. "Returns 'C', Buffer state: [_, _] (empty)" |
| 55 | +``` |
| 56 | + |
| 57 | +**Key Insight**: Notice how 'A' was automatically overwritten when 'C' was added, and we retrieved elements in chronological order: B (oldest remaining), then C (newest). |
| 58 | + |
| 59 | +### Real-World Example: Chat Application |
| 60 | + |
| 61 | +```smalltalk |
| 62 | +"Chat room that keeps last 4 messages for display" |
| 63 | +chatHistory := CTFIFOBuffer withCapacity: 4. |
| 64 | +
|
| 65 | +"Users send messages throughout the day" |
| 66 | +chatHistory push: 'Alok: Hello everyone!'. |
| 67 | +chatHistory push: 'Sebastian: Hey Alok, how are you?'. |
| 68 | +chatHistory push: 'Gordana: Great to see you both!'. |
| 69 | +chatHistory push: 'Alok: Sorry I was late to the party!'. |
| 70 | +
|
| 71 | +"Buffer now contains exactly 4 messages in chronological order" |
| 72 | +
|
| 73 | +"New message arrives - oldest automatically disappears" |
| 74 | +chatHistory push: 'Sebastian: No worries Alok, welcome!'. |
| 75 | +
|
| 76 | +"Display current message history (oldest to newest)" |
| 77 | +displayedMessages := OrderedCollection new. |
| 78 | +[ chatHistory isEmpty ] whileFalse: [ |
| 79 | + displayedMessages add: chatHistory pop |
| 80 | +]. |
| 81 | +
|
| 82 | +"displayedMessages now contains (in chronological order):" |
| 83 | +"1. 'Sebastian: Hey Alok, how are you?' (oldest remaining)" |
| 84 | +"2. 'Gordana: Great to see you both!'" |
| 85 | +"3. 'Alok: Sorry I was late to the party!'" |
| 86 | +"4. 'Sebastian: No worries Alok, welcome!' (newest)" |
| 87 | +
|
| 88 | +"Notice: Alok's first message was automatically removed to make space" |
| 89 | +``` |
| 90 | + |
| 91 | +**Why FIFO for Chat:** |
| 92 | +- Messages display in natural conversation flow |
| 93 | +- Oldest messages automatically scroll out of view |
| 94 | +- Zero manual memory management |
| 95 | +- Perfect for streaming conversation history |
| 96 | + |
| 97 | +## LIFO Buffer: Deep Dive with Example |
| 98 | + |
| 99 | +LIFO buffers work like a stack - the last element added is the first one retrieved. |
| 100 | + |
| 101 | +### Dry Run Example |
| 102 | +```smalltalk |
| 103 | +buffer := CTLIFOBuffer withCapacity: 2. |
| 104 | +buffer push: 'A'. "Buffer state: [A, _] readIndex=1, writeIndex=2" |
| 105 | +buffer push: 'B'. "Buffer state: [A, B] readIndex=2, writeIndex=1" |
| 106 | +buffer push: 'C'. "Buffer state: [C, B] readIndex=1, writeIndex=2 (A overwritten)" |
| 107 | +buffer pop. "Returns 'C', Buffer state: [_, B] readIndex=2" |
| 108 | +buffer pop. "Returns 'B', Buffer state: [_, _] (empty)" |
| 109 | +``` |
| 110 | + |
| 111 | +**Key Insight**: Elements are retrieved in reverse order - most recent first. This is perfect for "undo" scenarios and recent-item access patterns. |
| 112 | + |
| 113 | +### Real-World Example: Browser History Navigation |
| 114 | + |
| 115 | +```smalltalk |
| 116 | +"Browser that remembers last 3 visited pages" |
| 117 | +browserHistory := CTLIFOBuffer withCapacity: 3. |
| 118 | +
|
| 119 | +"User browses during the day" |
| 120 | +browserHistory push: 'https://pharo.org'. |
| 121 | +browserHistory push: 'https://github.com/pharo-containers'. |
| 122 | +browserHistory push: 'https://stackoverflow.com'. |
| 123 | +
|
| 124 | +"User clicks back button twice" |
| 125 | +previousPage := browserHistory pop. |
| 126 | +"Returns: 'https://stackoverflow.com' (most recent)" |
| 127 | +previousPage := browserHistory pop. |
| 128 | +"Returns: 'https://github.com/pharo-containers'" |
| 129 | +
|
| 130 | +"User visits new pages" |
| 131 | +browserHistory push: 'https://news.ycombinator.com'. |
| 132 | +browserHistory push: 'https://medium.com/programming-articles'. |
| 133 | +browserHistory push: 'https://reddit.com/r/programming'. "This overwrites 'https://pharo.org'!" |
| 134 | +
|
| 135 | +"User clicks back button - gets most recent first" |
| 136 | +previousPage := browserHistory pop. |
| 137 | +"Returns: 'https://reddit.com/r/programming' (newest)" |
| 138 | +previousPage := browserHistory pop. |
| 139 | +"Returns: 'https://medium.com/programming-articles'" |
| 140 | +previousPage := browserHistory pop. |
| 141 | +"Returns: 'https://news.ycombinator.com'" |
| 142 | +
|
| 143 | +"Notice: 'https://pharo.org' is gone - got overwritten when buffer was full!" |
| 144 | +``` |
| 145 | + |
| 146 | +**Why LIFO for Browser History:** |
| 147 | +- Back button should show most recent page first |
| 148 | +- Natural stack behavior matches user expectations |
| 149 | +- Automatically forgets old history when limit reached |
| 150 | +- Ideal for any "recent items" functionality |
| 151 | + |
| 152 | +## Performance Analysis: The Numbers Speak |
| 153 | + |
| 154 | +I conducted comprehensive performance tests comparing three approaches i.e Arrays, Circular Buffers & Ordered Collections for maintaining the last 100 items. Here's what the data reveals: |
| 155 | + |
| 156 | +### Test Run 1: Individual Performance |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | +The first screenshot shows a single test run where I measured the execution time for each approach. You can see in the transcript: |
| 161 | + |
| 162 | +- **Array**: 66ms |
| 163 | +- **Buffer**: 140ms |
| 164 | +- **OrderedCollection**: 299ms |
| 165 | + |
| 166 | +This shows the relative performance - OrderedCollection is clearly the slowest due to all those expensive removeFirst operations. |
| 167 | +### Test Run 2: Average Performance |
| 168 | + |
| 169 | + |
| 170 | + |
| 171 | +The second screenshot shows the average execution time over 100 test runs. The results are as follows: |
| 172 | + |
| 173 | +- **Array**: ~6 ms |
| 174 | +- **Buffer**: ~14 ms |
| 175 | +- **OrderedCollection**: ~31 ms |
| 176 | + |
| 177 | +These averages confirm the trends observed in the individual test run - Circular Buffers provide a solid middle ground between raw speed and ease of use, while Ordered Collections lag behind due to their inherent inefficiencies. |
| 178 | + |
| 179 | +### Test Run 3: Performance Benchmark |
| 180 | + |
| 181 | +Using Pharo's bench method to measure sustained performance, measured in operations per 5 seconds. The results are: |
| 182 | + |
| 183 | +- **Array**: ~756 iterations/5sec |
| 184 | +- **Buffer**: ~351 iterations/5sec |
| 185 | +- **OrderedCollection**: ~170 iterations/5sec |
| 186 | + |
| 187 | +### Why These Results Matter |
| 188 | + |
| 189 | +- **Array (Manual Management)** |
| 190 | + - Fastest performance |
| 191 | + - Requires manual index logic and careful boundary checks |
| 192 | + - Easy to introduce bugs & Hard to maintain |
| 193 | + |
| 194 | +- **Circular Buffer** |
| 195 | + - Nearly as fast as arrays |
| 196 | + - Delivers 43% of Array speed while eliminating 100% of the complexity |
| 197 | + - Automatically manages size |
| 198 | + - Clean, safe, and maintainable for most use cases |
| 199 | + |
| 200 | +- **OrderedCollection** |
| 201 | + - Slowest performance |
| 202 | + - Gets destroyed by `removeFirst` operations that shift hundreds of elements every time |
| 203 | + - Not suitable for high-volume data management |
0 commit comments