Memory
Pan back a year on a 1s chart and you're looking at ~31 million bars. Hold all of them and the tab dies. So the chart doesn't - it keeps a budget, and once it's over, it drops the bars furthest away from anything on screen. Pan back to them and they get fetched again, like any other page.
You mostly don't have to do anything for this. The rest of this page is how to tune it, what actually gets dropped, and how to cache the dropped bars so getting them back is free.
The budget
const { chart, element } = useDepthChart({
dataAdapter: myAdapter,
symbol: 'BTCUSDT',
initialLoad: { start, end },
horizon: end,
dataBudget: { maxBytes: 512 * 2 ** 20 }, // 512 MB
});| Prop | Type | Default | Description |
|---|---|---|---|
| dataBudget.maxBytes | number | 256 MB | Cap on the ohlcv bars the chart holds, across every symbol and pane. Infinity turns it off. |
It's in bytes, not bars, because that's what actually runs out. A bar is ~48 bytes in the chart's store, so 256 MB is a few million bars - plenty for anything but deep 1s pans.
When it goes over, it doesn't trim to just under the cap - it drops down to half of it. Otherwise every new live bar would set it off again.
What gets dropped
In order:
- Symbols no pane is showing. Switched away from something an hour ago? That goes first, whole.
- Bars far from every pane's view, from whichever end is furthest away.
What's never dropped: what each pane is looking at, a screen of pan room either side, and enough history before it for your indicators' lookback. During playback the playhead is kept too.
If what's on screen is already over the budget by itself (lots of panes, zoomed way out at a fine timeframe) the chart keeps it anyway and logs a warning once. It won't drop what you're looking at.
Note
Only ohlcv data counts toward the budget for now. L3, tick and footprint data aren't capped yet.
Holes
Here's the part that's different from other charts. Say you're live, pan back a year, and switch to 1s. Loading a year of seconds from there up to now would blow the budget, so the chart only loads around what you're looking at - and leaves a hole between that and the live edge.
While there's a hole:
- The chart is still live.
chart.isRealtime()stays true and the clock keeps running, the horizon stays at now. - The realtime feed pauses, since live bars would land on the far side of the hole.
- There's no price line, bid or ask line - the chart doesn't know the price at the horizon, and last year's close drawn there would look like the current price.
- Panning right pages forward into the hole, in one fetch per pan.
Once a forward fetch reaches now, the feed picks back up from where the data ends. Same thing happens if the budget drops the live edge because you've been looking at last year for a while.
Important
For the feed to pick back up, your adapter has to say hasMore: false on a forward fetch that
reaches now. Return hasMore: true forever and the chart never knows it caught up, so the feed
stays paused.
fetchBars: async ({ range, direction }) => {
const bars = await myApi(range.fromMs, range.toMs);
const hasMore = direction === 'forward' ? range.toMs < Date.now() : bars.length > 0;
return { bars, hasMore };
}Press play with a hole open and the chart loads a window at the horizon and plays from there - playback runs from the playhead, and the playhead is on the live side of the hole.
onEvict
Every chunk of bars that leaves memory goes through your adapter's onEvict first. That's not just
the budget - it's every reason the chart drops bars: a timeframe switch replacing the window, a
reload, a symbol getting unloaded, the chart being destroyed.
class MyAdapter implements IDataAdapter {
onEvict({ symbolInfo, timeframe, range, bars }: EvictedBars) {
myCache.put(symbolInfo.symbol, timeframe.barNs, range, bars.slice());
}
}| Prop | Type | Default | Description |
|---|---|---|---|
| symbolInforequired | SymbolInfo | - | Whose bars these are. |
| timeframerequired | Timeframe | - | The resolution the bars were fetched at - not necessarily what's on screen. |
| rangerequired | TimeRange | - | From the first bar's open to the last bar's close. |
| barsrequired | BarStore | - | The bars, as a read-only view onto the memory being freed. |
Only closed bars come through - the forming bar of a live feed never does.
Warning
bars is a view, not a copy - that's so dropping a few hundred MB doesn't first allocate another
few hundred MB. Copy what you keep (bars.slice()), or the whole buffer stays alive and nothing
got freed.
It's fire and forget. The chart doesn't wait for the promise you return, so if the user pans
straight back, fetchBars for that span can come in before your write has landed. Keep the pending
write around and answer from it. And writes started while the page is closing may never finish.
Then check your cache first in fetchBars, and panning back over dropped history costs nothing.
withOpfsCache
Or skip writing that yourself. withOpfsCache wraps any adapter and does exactly the above, on top
of the browser's origin private file system:
import { withOpfsCache } from '@christtrade/depth';
const adapter = withOpfsCache(new MyAdapter(), { maxBytes: 2 ** 30 });| Prop | Type | Default | Description |
|---|---|---|---|
| maxBytes | number | 1 GB | How much it keeps on disk. Oldest chunks get deleted first. |
| dir | string | "depth-cache" | The folder in the origin private file system. |
Evicted bars get written to disk, and fetchBars answers from there when the cache covers the
whole range it was asked for - everything else goes to your adapter like normal. What's cached
survives a reload, so the next session gets it too. Your adapter's own onEvict still gets called.
A few things worth knowing:
- Answers from the cache always say
hasMore: true. Whether there's more is your adapter's call, on the next fetch the cache can't answer. - A range that's only partly cached goes to your adapter in full. It doesn't stitch.
- Requests for supplemental bars skip the cache.
- Without OPFS (old browsers, Node) you get your adapter back unchanged.
It also adds clearCache(), for when your source corrected some history and the cached copy is
wrong:
await adapter.clearCache();Note
This is worth it for slow or rate-limited sources - a REST API that gives you 1000 bars a call. If your adapter is already fast, it's not doing much for you.