Skip to content

Commit 5ab1c2b

Browse files
authored
Merge pull request #6 from Alokzh/buffer-blogpost-file
Added docs for Circular Buffers implementation
2 parents b8774df + a765a2b commit 5ab1c2b

4 files changed

Lines changed: 203 additions & 0 deletions

File tree

docs/Circular_Buffer.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
![](Performance-Single-Run.png)
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+
![](Performance-Average.png)
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+
![](Performance-Bench.png)
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

docs/Performance-Average.png

203 KB
Loading

docs/Performance-Bench.png

162 KB
Loading

docs/Performance-Single-Run.png

151 KB
Loading

0 commit comments

Comments
 (0)