Skip to content

perf: Optimize Chart control performance with reduced allocations and improved algorithmic complexity#375

Open
PaulAndersonS wants to merge 1 commit into
mainfrom
paulandersons/perf-chart-control-optimizations
Open

perf: Optimize Chart control performance with reduced allocations and improved algorithmic complexity#375
PaulAndersonS wants to merge 1 commit into
mainfrom
paulandersons/perf-chart-control-optimizations

Conversation

@PaulAndersonS

Copy link
Copy Markdown
Collaborator

Root Cause of the Issue

Several hot paths in the Chart control used LINQ queries, List.Contains(), List.IndexOf(), and repeated GetType().Name reflection calls that introduced unnecessary allocations and O(n²) complexity in data-intensive rendering paths.

Description of Change

10 targeted micro-optimizations across 5 files to reduce allocations and improve algorithmic complexity in the Chart control:

  1. CategoryAxis.cs — O(n²) IndexOfDictionary lookup
    GroupData() called List.IndexOf() inside a LINQ select over every data point, making it O(n²). Replaced with a pre-built Dictionary<string, int> for O(1) index lookups, and used pre-allocated List<double> instead of LINQ materialization.

  2. CategoryAxis.csList.Contains()HashSet
    The deduplication loop in GroupData() used List.Contains() (O(n)) on every iteration. Replaced with a HashSet<string> seeded from existing values for O(1) membership checks.

  3. CartesianAxisLayout.cs — LINQ ToList() for single-item lookup → simple for loop
    GetAxisByName() created a full materialized List just to return the first match. Replaced with an early-return for loop that avoids all allocation and stops at the first match.

  4. CartesianChartArea.cs — Repeated GetType().Name reflection → cached result
    CalculateStackingValues() called GetType().Name.Contains(...) twice per data point inside nested loops (once per if branch). Cached the type name and the boolean result before the inner loop to eliminate repeated reflection.

  5. ErrorBarSegment.cs — 4× LINQ Where/Min/Max → single pass loops
    Computing xMin/xMax/yMin/yMax required 4 separate LINQ chains, each iterating the full collection. Replaced with 2 for loops that compute both min and max in a single pass per axis, with NaN fallback to 0.

  6. CartesianChartArea.cs — LINQ Where().Sum()for loop in GetYValue
    GetYValue() used a LINQ chain with closure allocation on every call. Replaced with a plain for loop over the pre-typed List<StackingSeriesBase>.

  7. CartesianSeries.cs — LINQ-generated index sequences → pre-allocated List<double>
    GetXValues() used (from val in list select (xIndexValues++)).ToList() to produce sequential [0, 1, 2, …] index lists, allocating an iterator and boxing closures. Replaced with a new List<double>(count) filled with a for loop, also removing the unused xIndexValues variable.

  8. ErrorBarSegment.cs — LINQ Where/Select/Min/Max for YRange → single loop
    The percentage-mode YRange calculation enumerated _topPointCollection and _bottomPointCollection 6 times (.Where, .Select, .Any, .Min, .Max × 2 collections). Replaced with two for loops that find the overall min/max in a single pass with a hasValid guard.

  9. ErrorBarSegment.cs — Eliminate list allocations in GetSdErrorValue
    GetSdErrorValue() allocated a filtered List, then two more List<double> (dev, sQDev) just to accumulate a sum of squares. Replaced with two inline loops that compute sum, count, and sumSqDev without any heap allocations.

  10. CartesianChartArea.csValues.ToList() inside loops → cache before loop
    UpdateSBS() and GetTotalWidth() called SideBySideSeriesPosition.Values.ToList() on every loop iteration, creating a fresh List snapshot each time. Moved the call outside the loop and iterated the cached list.

Issues Fixed

No specific issue — proactive performance improvements.

Screenshots

N/A — internal algorithmic changes with no visible behavior difference.

- Replace O(n²) IndexOf lookups with Dictionary in CategoryAxis.GroupData()
- Use HashSet for O(1) Contains checks in category value grouping
- Replace LINQ ToList() for single item with simple loop in GetAxisByName
- Cache GetType().Name result to avoid repeated reflection in stacking calculations
- Replace four LINQ Where/Min/Max queries with single loop in ErrorBarSegment
- Replace LINQ Where().Sum() with simple loop in stacking value calculation
- Replace LINQ-based index generation with pre-allocated List in GetXValues
- Replace LINQ Where/Select/Min/Max with single loop for YRange calculation
- Eliminate unnecessary List allocations in GetSdErrorValue
- Cache Values.ToList() result outside loops in SBS calculations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant