Releases: AndreCL/Oxyplot.DotNetAndroid
v.1.0.8
Major Optimizations
Benchmark Results: OxyPlot.DotNetAndroid
All benchmarks run on the host CLR (.NET 8, Windows) using BenchmarkDotNet 0.14.0 with
[MemoryDiagnoser].
Android JNI overhead cannot be measured off-device. Relative improvements are valid; absolute numbers will differ on ART.
FontMetricsBenchmark
Measures the cost of allocating a new FontMetrics-equivalent object per call (baseline, mirrors Paint.GetFontMetrics() no-arg overload) vs. filling a cached instance in-place (mirrors Paint.GetFontMetrics(Paint.FontMetrics) overload).
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|
GetFontMetrics_Allocating (baseline) |
2.944 ns | 0.100 ns | 0.200 ns | 1.00 | 0.0048 | 40 B | 1.00 |
GetFontMetrics_Cached (Fix 1) |
0.467 ns | 0.048 ns | 0.045 ns | 0.16 | – | 0 B | 0.00 |
Key finding: Caching the FontMetrics instance eliminates 40 bytes of allocation per call (100% reduction) and reduces call cost by 6.3×. At 10 views × 60 FPS with text on every frame, this removes ~24,000 allocations/second.
PathBuildBenchmark
Measures the cost of resetting and re-populating a path-like list structure (proxy for Android.Graphics.Path.Reset() + LineTo).
| Method | PointCount | Mean | Ratio | Gen0 | Allocated |
|---|---|---|---|---|---|
BuildPath_ReuseList (Fix 2 pattern) |
100 | 127.1 ns | 1.00 | – | 0 B |
BuildPath_NewList (naive) |
100 | 130.2 ns | 1.03 | 0.1023 | 856 B |
BuildPath_ReuseList |
500 | 583.5 ns | 1.00 | – | 0 B |
BuildPath_NewList |
500 | 681.5 ns | 1.17 | 0.4845 | 4,056 B |
BuildPath_ReuseList |
1000 | 1,163.6 ns | 1.00 | – | 0 B |
BuildPath_NewList |
1000 | 1,310.3 ns | 1.13 | 0.9680 | 8,104 B |
Key finding: Reusing a single path instance eliminates all managed allocations and is 3–17% faster depending on point count. For 500-point series (the stress test scenario), reuse saves 4 KB per draw call.
RenderContextBenchmark
Drives a no-op MockRenderContext through a simulated OnDraw cycle: axis rectangles + 500-point line series + 18 axis labels.
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
SimulatedFrame |
446.6 ns | 10.48 ns | 30.40 ns | 0.0715 | 600 B |
Key finding: A complete simulated frame (no Android JNI) allocates only 600 bytes at the OxyPlot+render-context layer. This is well within the target of <4 KB/frame. The remaining allocation is from OxyPlot's internal axis/series rendering pipeline.
PlotModelUpdateBenchmark
Calls IPlotModel.Update() on a PlotModel with one LineSeries of 500 points.
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
Update_WithData |
2,482.5 ns | 45.63 ns | 42.68 ns | 0.1793 | 1.48 KB |
Update_WithoutData |
573.1 ns | 9.95 ns | 12.58 ns | 0.1640 | 1.34 KB |
Key finding: The OxyPlot model update itself allocates ~1.3–1.5 KB per call regardless of updateData. This is OxyPlot library-internal overhead. The updateData: false path (used in the hot OnDraw loop after the first frame) is 4.3× faster.
Phase 3 Stress Test Results
Test: StressTest_10Views_60Hz_5Minutes — 10 models × 60 Hz × 300 seconds = 18,000,000 simulated renders via MockRenderContext.
Baseline (before fixes)
| Metric | Value |
|---|---|
| Elapsed | ~3.71 s (simulated time) |
| Avg frame time | 0.206 ms |
| Gen-0 GCs | 272 |
| Gen-1 GCs | 1 |
| Gen-2 GCs | 0 |
| Heap growth | 5,480 KB |
After fixes (Fix 1–5 applied)
| Metric | Value | vs Baseline |
|---|---|---|
| Elapsed | ~3.64 s | −1.8% |
| Avg frame time | 0.202 ms | −1.9% |
| Gen-0 GCs | 273 | +0.4% (within noise) |
| Gen-1 GCs | 1 | same |
| Gen-2 GCs | 0 | same |
| Heap growth | 5,576 KB | +1.7% (within noise) |
| Test result | ✅ PASSED | — |
Note: The stress test uses
MockRenderContext(no-op), so Android JNI costs (includingPaint.GetFontMetrics()JNI allocation) are not exercised. The GC numbers in the stress test reflect OxyPlot's own internal allocations only. The FontMetrics fix benefit will be visible on device as eliminated JNI-backed object allocations, not in this host-CLR benchmark.
Before / After Summary Table
| Benchmark / Metric | Before | After | Improvement |
|---|---|---|---|
GetFontMetrics allocated bytes/call |
40 B | 0 B | 100% reduction |
GetFontMetrics mean call time |
2.94 ns | 0.47 ns | 6.3× faster |
BuildPath allocated bytes/op (500 pts) |
4,056 B | 0 B | 100% reduction |
SimulatedFrame allocated bytes/frame |
600 B | 600 B | (no-op RC — host baseline) |
| Stress test gen-0 GCs / 5 min | 272 | 273 | within noise |
| Stress test gen-2 GCs / 5 min | 0 | 0 | ✅ |
| Stress test heap growth | 5,480 KB | 5,576 KB | within noise |
| Build passes | ✅ | ✅ | — |
| New compiler warnings | 0 | 0 | — |
Acceptance Criteria Status
| Criterion | Target | Result |
|---|---|---|
Allocated bytes per DrawText call (host proxy) |
0 B after fix | ✅ 0 B |
| Allocated bytes per simulated frame | < 4 KB | ✅ 600 B |
| GC gen-0 count / 5-min stress run | < 600 | ✅ 273 |
| GC gen-2 count / 5-min stress run | < 10 | ✅ 0 |
| Managed heap growth over 5-min run | < 10 MB | ✅ ~5.5 MB |
| Build passes after each fix | ✅ | ✅ |
| No new compiler warnings | ✅ | ✅ |