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
});
typescript
PropTypeDefaultDescription
dataBudget.maxBytesnumber256 MBCap 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:

  1. Symbols no pane is showing. Switched away from something an hour ago? That goes first, whole.
  2. 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 };
}
typescript

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());
    }
}
typescript
PropTypeDefaultDescription
symbolInforequiredSymbolInfo-Whose bars these are.
timeframerequiredTimeframe-The resolution the bars were fetched at - not necessarily what's on screen.
rangerequiredTimeRange-From the first bar's open to the last bar's close.
barsrequiredBarStore-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 });
typescript
PropTypeDefaultDescription
maxBytesnumber1 GBHow much it keeps on disk. Oldest chunks get deleted first.
dirstring"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();
typescript

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.

Was this page helpful?