You must be logged in to post your query.
Home › Forums › StockChart Support › Performance Regression Report
# Performance Regression Report: StockChart v1.15.13 vs v1.13.4
**Product:** CanvasJS StockChart
**Regression introduced in:** v1.14.x → v1.15.13
**Last working version:** v1.13.4 GA
**Severity:** Critical — interactive performance (pan/zoom/navigate) degraded by orders of magnitude
—
## Summary
Upgrading from v1.13.4 to v1.15.13 causes a severe and immediately noticeable
performance regression during any chart interaction (panning, zooming, dragging the
navigator slider). With a modest dataset of a few thousand data points, the chart
becomes completely unresponsive. Rolling back to v1.13.4 restores full performance
with identical configuration and data.
—
## Environment
– Browser: Chrome / Firefox / Edge (all affected)
– Chart type: StockChart with line, area, and scatter series
– Data volume: ~1,000–50,000 data points total across multiple series
– Navigator: enabled with dynamicUpdate: true
– animationEnabled: false
—
## Steps to Reproduce
1. Create a StockChart with 3–4 line/area series sharing the same X axis (e.g.
time-series price data + indicator lines), plus several scatter series.
2. Load ~5,000+ data points per series.
3. Load v1.13.4 — drag the navigator slider. Observe smooth, responsive updates.
4. Load v1.15.13 with identical configuration — drag the same slider.
Observe complete unresponsiveness / extreme lag.
No configuration changes are required to reproduce the issue.
—
## Root Cause — Confirmed via Source Analysis
We reverse-engineered both minified builds to identify the exact source of the
regression. The root cause is an **O(N²) algorithm introduced in
_processMultiseriesPlotUnit**, called on every single interaction frame.
### _processMultiseriesPlotUnit — O(N) → O(N²)
This function runs during Chart._initialize(), which is called on every
mousemove event while the user drags the navigator or pans the chart.
It processes all data series to compute axis range information.
**v1.13.4 — O(N), one pass per series (fast):**
`javascript
// minDiff computed inline during the main data loop — O(N) total
if (0 < g) {
d = b – F.dataPoints[g – 1].x;
0 > d && (d *= -1);
c.minDiff > d && 0 !== d && (c.minDiff = d)
}
`
**v1.15.13 — O(N²), separate deduplication pass (catastrophically slow):**
`javascript
// Main loop: pushes every x-value into array E — O(N)
E.push(b);
// After the main loop: deduplicates E using indexOf — O(N²) !!
for (r = 0; r < E.length; r++)
-1 === f.indexOf(E[r]) && f.push(E[r]); // indexOf is O(N) per iteration
f.sort(function (a, b) { return a – b }); // O(N log N)
for (r = 1; r < f.length; r++) // O(N)
k = f[r] – f[r – 1], c.minDiff > k && (c.minDiff = k);
`
### Why indexOf-based deduplication is O(N²)
For each of the N elements in array E, Array.prototype.indexOf performs a
linear scan of array f, which grows up to N elements. Total comparisons:
**N × N = N²**.
### Real-world impact with typical data
| Data points per series | Series count | Total E length | indexOf comparisons |
|—|—|—|—|
| 1,000 | 4 | 4,000 | **16,000,000** |
| 5,000 | 4 | 20,000 | **400,000,000** |
| 10,000 | 4 | 40,000 | **1,600,000,000** |
This runs on **every mousemove event** during navigator drag. At 60 fps that
is 60 executions per second. With 5,000 data points and 4 series, the browser
must perform 24 billion comparisons per second — an impossible workload.
### Why it is worse with multiple series sharing an X axis
The E array accumulates x-values from **all series** in the same plot unit.
If 4 line series each have 5,000 data points with the same timestamps, E
has 20,000 entries with massive duplication, making the deduplication even more
expensive.
—
## Secondary Issue: _syncCharts restructured from 1 loop to 3 loops
In addition to the O(N²) issue, _syncCharts() (also called on every
mousemove) was refactored from a single combined loop into three sequential
passes over all charts:
**v1.13.4** — single loop per chart: _initialize → setLayout → renderElements
**v1.15.13** — three separate loops:
1. All charts: _initialize() + setLayout()
2. All charts: _getPlotAreaInfo() ← **new redundant pass**
3. All charts: renderElements()
While loop 2 was introduced to support cross-chart axis alignment (a valid
correctness fix), it can be merged into loop 1 without losing the alignment
feature — exactly as setChartsLayout() does in the same codebase.
—
## Fix We Applied (Client-Side Workaround)
We patched the minified build directly with two surgical string replacements:
**Fix 1 — O(N²) → O(N) deduplication in _processMultiseriesPlotUnit:**
`javascript
// Before (O(N²)):
for (r = 0; r < E.length; r++)
-1 === f.indexOf(E[r]) && f.push(E[r]);
// After (O(N) using Set):
f = Array.from(new Set(E));
`
**Fix 2 — _syncCharts: 3 loops → 2 loops:**
Moved the _getPlotAreaInfo() call inline into loop 1, eliminating the
separate second pass. This matches the existing setChartsLayout() pattern
already present in v1.15.13 itself.
After applying both fixes, interactive performance is restored to parity with
v1.13.4.
—
## Expected vs Actual Behavior
**Expected:** Interactive performance equal to or better than v1.13.4 with the
same configuration and data.
**Actual:** Completely unresponsive interaction with datasets of a few thousand
data points per series. Scales quadratically worse as data size increases.
—
## Request
1. **Critical:** Replace indexOf-based deduplication in
_processMultiseriesPlotUnit with a Set-based approach:
`javascript
// O(N) replacement for the O(N²) loop:
f = Array.from(new Set(E));
f.sort(function (a, b) { return a – b });
// … minDiff loop unchanged
`
2. **Minor:** In _syncCharts, merge the _getPlotAreaInfo pass into the
init+layout loop (as done in setChartsLayout), eliminating the redundant
third pass over all charts.
Thank you for investigating this issue.
You must be logged in to reply to this topic.