znlgis 博客

GIS开发与技术分享

第十九章:调试测试与性能优化

19.1 测试框架

19.1.1 测试项目结构

test/
├── LightCAD.MathLib.Tests/      # 数学库单元测试
│   ├── LightCAD.MathLib.Tests.csproj
│   ├── Point2dTests.cs
│   ├── Vector2dTests.cs
│   ├── Matrix4dTests.cs
│   ├── IntersectionTests.cs
│   ├── BezierCurveTests.cs
│   └── NurbsCurveTests.cs
└── WinFormTest/                 # 窗体功能测试
    ├── WinFormTest.csproj
    └── ViewportTests.cs

19.1.2 测试框架选择

LightCAD使用MSTest作为单元测试框架:

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.x" />
<PackageReference Include="MSTest.TestAdapter" Version="3.x" />
<PackageReference Include="MSTest.TestFramework" Version="3.x" />

19.2 单元测试实践

19.2.1 数学库测试

[TestClass]
public class Point2dTests
{
    [TestMethod]
    public void Distance_Origin_To_3_4_Should_Be_5()
    {
        var p1 = new Point2d(0, 0);
        var p2 = new Point2d(3, 4);
        Assert.AreEqual(5.0, p1.DistanceTo(p2), 1e-10);
    }

    [TestMethod]
    public void MidPoint_Should_Be_Average()
    {
        var p1 = new Point2d(0, 0);
        var p2 = new Point2d(100, 200);
        var mid = p1.MidPoint(p2);
        Assert.AreEqual(50.0, mid.X, 1e-10);
        Assert.AreEqual(100.0, mid.Y, 1e-10);
    }

    [TestMethod]
    public void IsEqualTo_WithinTolerance_Should_Return_True()
    {
        var p1 = new Point2d(1.0, 2.0);
        var p2 = new Point2d(1.0 + 1e-11, 2.0 - 1e-11);
        Assert.IsTrue(p1.IsEqualTo(p2));
    }

    [TestMethod]
    public void IsEqualTo_OutsideTolerance_Should_Return_False()
    {
        var p1 = new Point2d(1.0, 2.0);
        var p2 = new Point2d(1.1, 2.0);
        Assert.IsFalse(p1.IsEqualTo(p2));
    }
}

[TestClass]
public class Matrix4dTests
{
    [TestMethod]
    public void Identity_Transform_Should_Not_Change_Point()
    {
        var point = new Point3d(10, 20, 30);
        var result = Matrix4d.Identity.Transform(point);
        Assert.AreEqual(10, result.X, 1e-10);
        Assert.AreEqual(20, result.Y, 1e-10);
        Assert.AreEqual(30, result.Z, 1e-10);
    }

    [TestMethod]
    public void Translation_Should_Move_Point()
    {
        var point = new Point3d(0, 0, 0);
        var matrix = Matrix4d.CreateTranslation(10, 20, 30);
        var result = matrix.Transform(point);
        Assert.AreEqual(10, result.X, 1e-10);
        Assert.AreEqual(20, result.Y, 1e-10);
        Assert.AreEqual(30, result.Z, 1e-10);
    }

    [TestMethod]
    public void RotationZ_90Degrees_Should_Rotate_Correctly()
    {
        var point = new Point3d(1, 0, 0);
        var matrix = Matrix4d.CreateRotationZ(Math.PI / 2);
        var result = matrix.Transform(point);
        Assert.AreEqual(0, result.X, 1e-10);
        Assert.AreEqual(1, result.Y, 1e-10);
        Assert.AreEqual(0, result.Z, 1e-10);
    }

    [TestMethod]
    public void Inverse_Times_Original_Should_Be_Identity()
    {
        var matrix = Matrix4d.CreateTranslation(10, 20, 30)
            * Matrix4d.CreateRotationZ(Math.PI / 4)
            * Matrix4d.CreateScale(2, 3, 1);

        var inverse = matrix.Inverse();
        var product = matrix * inverse;

        // 验证结果接近单位矩阵
        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                var expected = (i == j) ? 1.0 : 0.0;
                Assert.AreEqual(expected, product[i, j], 1e-10);
            }
        }
    }
}

[TestClass]
public class IntersectionTests
{
    [TestMethod]
    public void LineLineIntersect_Perpendicular()
    {
        var result = LineLineIntersect.Compute(
            new Point2d(0, 0), new Vector2d(1, 0),
            new Point2d(5, -5), new Vector2d(0, 1)
        );

        Assert.IsNotNull(result);
        Assert.AreEqual(5.0, result.Value.X, 1e-10);
        Assert.AreEqual(0.0, result.Value.Y, 1e-10);
    }

    [TestMethod]
    public void LineLineIntersect_Parallel_Returns_Null()
    {
        var result = LineLineIntersect.Compute(
            new Point2d(0, 0), new Vector2d(1, 0),
            new Point2d(0, 1), new Vector2d(1, 0)
        );

        Assert.IsNull(result);
    }

    [TestMethod]
    public void LineCircleIntersect_Two_Points()
    {
        var results = LineCircleIntersect.Compute(
            new Point2d(-10, 0), new Vector2d(1, 0),
            new Point2d(0, 0), 5
        );

        Assert.AreEqual(2, results.Count);
    }

    [TestMethod]
    public void LineCircleIntersect_Tangent()
    {
        var results = LineCircleIntersect.Compute(
            new Point2d(0, 5), new Vector2d(1, 0),
            new Point2d(0, 0), 5
        );

        Assert.AreEqual(1, results.Count);
        Assert.AreEqual(0, results[0].X, 1e-6);
        Assert.AreEqual(5, results[0].Y, 1e-6);
    }

    [TestMethod]
    public void LineCircleIntersect_No_Intersection()
    {
        var results = LineCircleIntersect.Compute(
            new Point2d(0, 10), new Vector2d(1, 0),
            new Point2d(0, 0), 5
        );

        Assert.AreEqual(0, results.Count);
    }
}

19.2.2 运行测试

# 运行所有测试
dotnet test LightCAD1.sln

# 运行特定测试项目
dotnet test test/LightCAD.MathLib.Tests/LightCAD.MathLib.Tests.csproj

# 运行特定测试类
dotnet test --filter "ClassName=LightCAD.MathLib.Tests.Point2dTests"

# 运行特定测试方法
dotnet test --filter "FullyQualifiedName~Distance_Origin_To_3_4"

# 生成代码覆盖率报告
dotnet test --collect:"XPlat Code Coverage"

19.3 调试技巧

19.3.1 断点策略

几何计算断点

在数学库的关键计算位置设置断点:

  • 交点计算结果
  • 曲线参数求值
  • 矩阵变换结果

渲染调试断点

在渲染管线的关键位置设置断点:

  • 几何体创建
  • 材质设置
  • 场景更新

输入处理断点

在输入系统的关键位置设置断点:

  • 鼠标坐标转换
  • 捕捉点计算
  • 命令执行

19.3.2 日志系统

public static class Logger
{
    private static readonly string logDir = "Logs";
    private static StreamWriter writer;

    public static void Initialize()
    {
        Directory.CreateDirectory(logDir);
        var logFile = Path.Combine(logDir,
            $"lightcad_{DateTime.Now:yyyyMMdd_HHmmss}.log");
        writer = new StreamWriter(logFile, true);
    }

    public static void Debug(string message)
    {
        Log("DEBUG", message);
    }

    public static void Info(string message)
    {
        Log("INFO", message);
    }

    public static void Warning(string message)
    {
        Log("WARN", message);
    }

    public static void Error(string message, Exception ex = null)
    {
        Log("ERROR", message);
        if (ex != null)
        {
            Log("ERROR", $"Exception: {ex.Message}");
            Log("ERROR", $"StackTrace: {ex.StackTrace}");
        }
    }

    private static void Log(string level, string message)
    {
        var line = $"[{DateTime.Now:HH:mm:ss.fff}] [{level}] {message}";
        writer?.WriteLine(line);
        writer?.Flush();
        System.Diagnostics.Debug.WriteLine(line);
    }
}

19.3.3 可视化调试

/// <summary>
/// 调试辅助:在视口中绘制调试几何
/// </summary>
public static class DebugDraw
{
    private static List<TemporaryGraphic> debugGraphics = new();

    public static void DrawPoint(Point2d point, LcColor color)
    {
        debugGraphics.Add(new TemporaryGraphic
        {
            Type = TempGraphicType.Point,
            Center = point,
            Color = color
        });
    }

    public static void DrawLine(Point2d start, Point2d end, LcColor color)
    {
        debugGraphics.Add(new TemporaryGraphic
        {
            Type = TempGraphicType.Line,
            Points = new[] { start, end },
            Color = color
        });
    }

    public static void DrawBounds(BoundingBox2d bounds, LcColor color)
    {
        DrawLine(bounds.Min, new Point2d(bounds.Max.X, bounds.Min.Y), color);
        DrawLine(new Point2d(bounds.Max.X, bounds.Min.Y), bounds.Max, color);
        DrawLine(bounds.Max, new Point2d(bounds.Min.X, bounds.Max.Y), color);
        DrawLine(new Point2d(bounds.Min.X, bounds.Max.Y), bounds.Min, color);
    }

    public static void Clear()
    {
        debugGraphics.Clear();
    }
}

19.4 性能优化

19.4.1 内存优化

对象池

public class ObjectPool<T> where T : new()
{
    private readonly ConcurrentBag<T> pool = new();
    private readonly int maxSize;

    public ObjectPool(int maxSize = 1000)
    {
        this.maxSize = maxSize;
    }

    public T Rent()
    {
        return pool.TryTake(out var item) ? item : new T();
    }

    public void Return(T item)
    {
        if (pool.Count < maxSize)
        {
            pool.Add(item);
        }
    }
}

// 使用示例:复用Point2d列表
var pointListPool = new ObjectPool<List<Point2d>>();
var points = pointListPool.Rent();
try
{
    // 使用points...
}
finally
{
    points.Clear();
    pointListPool.Return(points);
}

19.4.2 渲染优化

批量渲染

public class BatchRenderer
{
    /// <summary>
    /// 将多条线段合并为单次绘制调用
    /// </summary>
    public void DrawLinesBatch(List<LcLine> lines, LcColor color)
    {
        var positions = new float[lines.Count * 6]; // 每条线2个点×3坐标

        for (int i = 0; i < lines.Count; i++)
        {
            positions[i * 6 + 0] = (float)lines[i].StartPoint.X;
            positions[i * 6 + 1] = (float)lines[i].StartPoint.Y;
            positions[i * 6 + 2] = 0;
            positions[i * 6 + 3] = (float)lines[i].EndPoint.X;
            positions[i * 6 + 4] = (float)lines[i].EndPoint.Y;
            positions[i * 6 + 5] = 0;
        }

        var geometry = new BufferGeometry();
        geometry.SetAttribute("position",
            new BufferAttribute(positions, 3));

        var material = new LineBasicMaterial
        {
            Color = ConvertColor(color)
        };

        scene.Add(new LineSegments(geometry, material));
    }
}

空间索引

public class SpatialIndex
{
    private QuadTree<LcEntity> quadTree;

    public SpatialIndex(BoundingBox2d worldBounds)
    {
        quadTree = new QuadTree<LcEntity>(worldBounds, maxDepth: 8);
    }

    public void Insert(LcEntity entity)
    {
        quadTree.Insert(entity, entity.Bounds);
    }

    public void Remove(LcEntity entity)
    {
        quadTree.Remove(entity);
    }

    /// <summary>
    /// 范围查询(比遍历所有实体快得多)
    /// </summary>
    public IEnumerable<LcEntity> Query(BoundingBox2d searchBounds)
    {
        return quadTree.Query(searchBounds);
    }
}

19.4.3 性能分析

public class PerformanceMonitor
{
    private Dictionary<string, List<long>> timings = new();
    private Stopwatch stopwatch = new();

    /// <summary>
    /// 计时开始
    /// </summary>
    public IDisposable MeasureTime(string operationName)
    {
        return new TimeMeasurement(this, operationName);
    }

    private class TimeMeasurement : IDisposable
    {
        private PerformanceMonitor monitor;
        private string name;
        private long startTicks;

        public TimeMeasurement(PerformanceMonitor monitor, string name)
        {
            this.monitor = monitor;
            this.name = name;
            this.startTicks = Stopwatch.GetTimestamp();
        }

        public void Dispose()
        {
            var elapsed = Stopwatch.GetTimestamp() - startTicks;
            var elapsedMs = elapsed * 1000 / Stopwatch.Frequency;

            if (!monitor.timings.ContainsKey(name))
                monitor.timings[name] = new List<long>();
            monitor.timings[name].Add(elapsedMs);
        }
    }

    /// <summary>
    /// 输出性能报告
    /// </summary>
    public void PrintReport()
    {
        foreach (var (name, times) in timings)
        {
            var avg = times.Average();
            var max = times.Max();
            var min = times.Min();
            Console.WriteLine(
                $"{name}: 平均={avg:F2}ms, 最大={max}ms, 最小={min}ms, " +
                $"调用次数={times.Count}");
        }
    }
}

// 使用示例
var monitor = new PerformanceMonitor();

using (monitor.MeasureTime("渲染场景"))
{
    viewportRenderer.Render(viewport, document);
}

using (monitor.MeasureTime("视图构建"))
{
    viewBuilder.BuildView(buildParams);
}

monitor.PrintReport();

19.5 常见问题排查

19.5.1 渲染问题

问题 可能原因 解决方法
图元不显示 图层被冻结/关闭 检查图层状态
3D模型黑色 法线方向错误 重新计算法线
闪烁 Z-fighting 增加深度偏移
性能低 实体过多 启用LOD/空间索引

19.5.2 几何计算问题

问题 可能原因 解决方法
交点丢失 容差设置不当 调整Tolerance值
布尔运算失败 退化几何体 检查输入有效性
变换结果错误 矩阵乘法顺序 检查变换组合顺序

19.6 本章小结

本章介绍了LightCAD的测试、调试和性能优化方法。单元测试覆盖了数学库的核心功能,确保几何计算的正确性。调试技巧包括断点策略、日志系统和可视化调试。性能优化涵盖了对象池、批量渲染、空间索引等技术。性能监控工具帮助开发者识别和解决性能瓶颈。


上一章第十八章:标注与文本系统

下一章第二十章:实战案例与二次开发指南