Prologue: 09:00:00 AM — When the Trading Screen Freezes
At 9:00 AM sharp when the equity market opens, a trader's workstation typically displays a watchlist grid with hundreds of symbols, multiple real-time 10-level orderbook windows, a 60-symbol multi-quote matrix, and a full-market time-and-sales ticker streaming executions like a waterfall.
A battle-tested Win32/MFC desktop trading platform (HTS) that runs smoothly during normal market hours often suffers from unresponsive mouse clicks or 1–2 second window-switching stalls (UI Freezing) during the 09:00:00 – 09:05:00 market open peak. With the arrival of a multi-exchange environment where both the Primary Exchange and Alternative Trading System (ATS) broadcast market data simultaneously, real-time packet volume has more than doubled.
In this 6-part series, we share the complete engineering journey of embedding a Lock-Free RCU/CoW in-memory market data engine (MarketDataEngine) and a decoupled UI rendering pipeline into a multi-million-line legacy C++/MFC codebase without a full rewrite—reducing peak UI freeze time by 99% (from 1,531ms to 15ms) and nearly tripling real-time throughput.
In Part 1, we dissect why traditional Win32 event-driven architectures collapse under ultra-high-frequency real-time streams.
1. Anatomy of the Legacy Market Data Pipeline (AS-IS)
Most long-running Win32/MFC client architectures were designed around the Windows message queue (PostMessage / SendMessage).
flowchart LR
subgraph NetThread["Network Ingestion Layer"]
A["Socket Receive Buffer"] --> B["Frame Decoder / Decompression"]
end
subgraph Router["SubscriptionRouter"]
B --> C["Lookup Subscribed Views (HWND)"]
C -->|"PostMessage per packet"| D["Win32 UI Message Queue (Limit: 10,000)"]
end
subgraph UIThread["Single Main UI Thread (Bottleneck)"]
D --> E["OnRealtimePacket(WM_USER + N)"]
E --> F["1. Big-Endian → Little-Endian Byte Swap"]
F --> G["2. Change/Rate Calculation & Thousands Formatting"]
G --> H["3. Immediate GDI / ActiveX Cell Redraw"]
end
At first glance, this event-driven structure looks simple and intuitive. However, the moment tens of thousands of packets per second flood in while the user has multiple heavy screens open simultaneously, four critical bottlenecks emerge.
2. The 4 Structural Bottlenecks Behind Market Open Freezing
Bottleneck ①: Win32 Message Queue Saturation & PostMessage Overhead
In Windows OS, the default message queue limit for each GUI thread is 10,000 messages (USERPostMessageLimit).
- When thousands of trade and orderbook packets arrive per second right after market open, if 5 views (
HWND) subscribe to the same symbol, the router triggers 5 kernel transitions viaPostMessageper packet. - When thousands of custom market data messages (
WM_USER + α) pack the front of the queue, user mouse clicks (WM_LBUTTONDOWN), keyboard input, and timer messages (WM_TIMER) are pushed to the back of the queue and starved for hundreds of milliseconds to several seconds.
Bottleneck ②: $N$-Way Redundant Deserialization & Endian Conversion
Financial wire packets arriving over the network are typically compressed in Network Byte Order (Big-Endian) and fixed-length ASCII strings.
- In the legacy design, each screen (
WatchlistView,OrderBookView,MultiQuoteCellWnd, etc.) received a packet pointer or copy and independently repeated 4-byte/8-byte endian swaps (_byteswap_ulong,_byteswap_uint64) and struct parsing inside its own UI handler. - When a single orderbook packet arrived for a major stock, four open screens executed the exact same byte-swapping and price reconstruction logic four times on the main UI thread.
Bottleneck ③: String Formatting & Heap Allocations on the Main UI Thread
Populating a single grid cell on a trading screen requires more computation than meets the eye:
- Sign classification (limit-up/up/flat/down/limit-down), floating-point percentage change calculation ($\frac{\text{Price} - \text{RefPrice}}{\text{RefPrice}} \times 100$), thousands-separator comma insertion (
sprintf/CString::Format), and turnover unit scaling are all executed on the main UI thread. - With 1,000 trades per second across a 200-symbol watchlist, the UI thread wastes precious CPU cycles assembling strings and allocating/freeing
CStringheap buffers.
Bottleneck ④: Tight Coupling Between Packet Arrival Rate and Monitor Refresh Rate (FPS)
The human eye cannot perceive numerical updates faster than 20–30 frames per second (33–50ms intervals).
- Yet the legacy pipeline pushed cell values (
SetCell/put_Cell) and invoked GDIInvalidateRectimmediately on every single packet arrival (Event-Driven). - In COM-based ActiveX grids (
VSFlexGridfamily) or multi-pane windows with dozens of child controls, updating cells at 1ms intervals triggers cascading internal layout recalculations and synchronous GDI repaints—blocking a single frame for 30ms–100ms.
3. "Don't Guess, Measure" — Designing a Non-Intrusive Micro-Profiler
Before touching the architecture, our first step was embedding a lightweight micro-profiler (EngineProfiler) to measure "where every microsecond ($\mu s$) is spent" under production-identical conditions.
To ensure the profiler itself added near-zero overhead, we followed three principles:
- QPC (
QueryPerformanceCounter) Microsecond Timing: Captures tens-of-$\mu s$ parsing, binding, and painting latencies invisible toGetTickCount()(15.6ms resolution). - Thread-Local / Atomic Counter Accumulation + Periodic CSV Flush: Avoids per-tick file I/O by accumulating into
std::atomic<uint64_t>counters and flushing to disk only once every 1–5 seconds. - INI Runtime Switch (
Profiler.Enable=0/1): Remains at0in normal operation and flips to1during market open benchmark runs to compare AS-IS vs. TO-BE on identical symbol sets.
// RAII-based microsecond scope timer
struct ScopedTimerUs {
std::atomic<uint64_t>& totalUs_;
std::atomic<uint64_t>& maxUs_;
LARGE_INTEGER start_;
ScopedTimerUs(std::atomic<uint64_t>& totalUs, std::atomic<uint64_t>& maxUs)
: totalUs_(totalUs), maxUs_(maxUs) {
::QueryPerformanceCounter(&start_);
}
~ScopedTimerUs() {
LARGE_INTEGER end, freq;
::QueryPerformanceCounter(&end);
::QueryPerformanceFrequency(&freq);
const uint64_t elapsedUs =
static_cast<uint64_t>(end.QuadPart - start_.QuadPart) * 1'000'000ULL /
static_cast<uint64_t>(freq.QuadPart);
totalUs_.fetch_add(elapsedUs, std::memory_order_relaxed);
uint64_t prevMax = maxUs_.load(std::memory_order_relaxed);
while (elapsedUs > prevMax &&
!maxUs_.compare_exchange_weak(prevMax, elapsedUs, std::memory_order_relaxed)) {}
}
};
Profiling the first 9 minutes (540 seconds) after market open revealed the stark reality of the AS-IS pipeline:
- In a single watchlist window alone, maximum single-tick processing time spiked to
1,531ms(1.53 seconds of complete UI freeze), - Noticeable stalls exceeding 100ms (
>100ms Freeze) occurred 74 times, and frame drops exceeding 33ms (30fps threshold) occurred 257 times, - While the UI thread was busy painting earlier packets, subsequent market data notifications backed up—throttling effective UI event throughput to roughly 1/3 of actual market activity.
4. The 4 Core Goals of Our Architectural Redesign
With the root causes quantified, our engineering goals became crystal clear. To fundamentally transform performance without breaking hundreds of existing screen modules, we established four design pillars:
- Complete Decoupling of Network, Cache, and UI:
Upon packet arrival, background workers perform endian conversion and normalization exactly once to update a central in-memory snapshot (SymbolSnapshot). - Wait-Free Reads via Lock-Free RCU & Copy-on-Write:
Even while background workers write tens of thousands of updates per second, the UI thread reads the latest immutable snapshot with zero mutex blocking. - Decoupling Producer Rate from Consumer Frame Rate (Conflation & Frame Budget):
Even if a stock updates 10,000 times per second, the UI coalesces intermediate ticks and renders only the latest state at human-perceptible frame rates (30–50ms). - 100% Safe Dual-Path Architecture:
A single runtime configuration flag (UseDataManager=0/1) switches seamlessly between the legacy path and the new engine path, keeping operational rollout risk at zero.
In [Part 2], we dive into the C++17 implementation of the engine's heart: Symbol Hash Sharding and Lock-Free RCU (RcuSnapshotSlots) + Copy-on-Write in-memory cache architecture.