浅谈window桌面GUI技术及图像渲染性能测试实践_winui渲染线程-程序员宅基地

技术标签: 性能测试  性能  GUI  

一、Windows的图形界面架构

从Windows Vista之后,Desktop composition的部分就由Desktop Window Manager完成了(当然是启用Aero的情况下,Windows 8起DWM是必须开启的
Desktop Window Manager
如上图,应用程序画完了界面,告诉DWM把它放到桌面上去

DWM本身是基于Direct3D的,D3D下面是WDDM驱动
Direct3D

至于应用程序,绝大多数win桌面应用都是基于GDI的,很老的图形库 (从某个版本起GDI也是跑在D3D之上了,于是显卡厂家就不用写GDI驱动了),D3D(比如基于WPF的应用,今天主要介绍的应用),OpenGL(现在的Windows的图形架构是以DirectX为主,OpenGL支持需要OpenGL installable client driver)

二、window图像渲染的基本流程

从程序中提交一个Draw,数据需要经过:

A p p − > D X r u n t i m e − > U s e r m o d e d r i v e r − > d x g k r n l − > K e r n e l m o d e d r i v e r − > G P U App->DX runtime->User mode driver->dxgkrnl->Kernel mode driver->GPU App>DXruntime>Usermodedriver>dxgkrnl>Kernelmodedriver>GPU

在到达GPU之前,全都是在CPU上执行的,所以从程序本身是无法获取渲染结果

到这里就为我们做window桌面程序图像渲染性能测试带来两个问题:

  1. 怎么检查图像渲染的质量?
  2. 怎么获取图像渲染的响应时间?

由于需要桌面UI自动化测试的技术,所以下面我们介绍window桌面程序UI自动化测试技术

三、window桌面程序UI自动化测试技术

桌面GUI自动化测试技术

3.1、Win32程序

使用 Win32 API 来创建的程序成为Win32程序。
提供 Win32 API的dll被加载到应用程序的进程中,应用程序通过这些API来创建线程、窗口和控件。
Win32程序中,所有窗口和控件都是一个窗口类的实例,都拥有一个窗口句柄,窗口对象属于内核对象,由Windows子系统来维护。

Windows子系统为标准控件定义了窗口类,并使用GDI来绘制这些标准控件。

Win32程序采用消息循环机制:
Win32程序采用消息循环机制

3.2、WPF程序

WPF的控件不再是通过Win32 API来创建窗口,使用Win32 API并不能查找和操作WPF控件
WPF所有控件和动画都是使用DirectX 绘制
WPF控件不直接支持MSAA,而是通过 UIA 用桥转换技术来支持MSAA
WPF用AutomationPeer类支持自动化,每一种控件都有对应的 AutomationPeer类。AutomationPeer不直接暴露给测试客户端,而是通过UIA来使用。UIA向应用程序窗口发送WM_GetObject消息,获得由AutomationPeer实现的UIA Server端Provider。AutomationPeer由控件创建(OnCreateAutomationPeer)

3.3、UIAutomation

UIAutomation是微软从Windows Vista开始推出的一套全新UI自动化测试技术, 简称UIA。

UIA定义了全新的、针对UI自动化的接口和模式。测试程序可以通过这些接口来查找和操作控件。
遍历和条件化查询:TreeWalker/FindAll
UI元素属性的UIA Property, 包括Name、 ID、Type、ClassName、Location、 Visibility等等。
UIA Pattern(控件的行为模式), 比如Select、Expand、Resize、 Check、Value等等。

UIA的两种实现方法:
Server-Side Provider:
由被测程序实现UIA定义的接口,返回给测试程序。WPF程序通过这种方式来支持UIA。

Client-Side Provider:
测试程序没有实现UIA定义的接口。由UIA Runtime或测试程序自己来实现。比如Win32和WinForm程序,UIA Runtime通过MSAA来实现UIA定义的接口。UIA定义了全新的、针对UI自动化的接口和模式。测试程序可以通过这些接口来查找和操作控件。

遍历和条件化查询:TreeWalker/FindAll
UI元素属性的UIA Property, 包括Name、 ID、Type、ClassName、Location、 Visibility等等。
UIA Pattern(控件的行为模式), 比如Select、Expand、Resize、 Check、Value等等。

UIA驱动计算器示例:

using System.Windows.Automation;
PropertyCondition conditionName = new PropertyCondition(AutomationElement.NameProperty, "计算器");
AutomationElement calcWindow =  AutomationElement.RootElement.FindFirst(TreeScope.Children, conditionName);
//Button 1
PropertyCondition conditionBtn1 = new PropertyCondition(AutomationElement.AutomationIdProperty, "131");
AutomationElement button1 = calcWindow.FindFirst(TreeScope.Descendants, conditionBtn1);
//点击Button1
InvokePattern invokePatternBtn1 = (InvokePattern)button1.GetCurrentPattern(InvokePattern.Pattern);
invokePatternBtn1.Invoke();

四、桌面程序图像渲染性能测试实践

因为我们的性能测试是基于部分UI自动化测试技术落地的,在此介绍一下我们的UI自动化测试解决方案
UI自动化测试解决方案

测试解决方案应至少包括5个项目,其中前两个是和其他测试解决方案共享的。5个项目均为类库,不能直接执行。

  • AI.Robot为UI驱动框架。 AI.Utilities项目里是一些辅助类,如数据库读写、图片对比等(性能测试需用到)。
  • AI.App.UIObjects项目里放置UI对象。把UI对象集中放置到此项目中是为了减少界面更改带来的维护工作量。
  • AI.App.BusinessLogic项目里放置可重复用到的界面元素操作的集合,通常是为了完成一项特定的业务的步骤的集合。
  • AI.App.TestCases里放置测试用例。并按照MSTest单元测试框架组织测试类和测试方法。包含测试类和测试方法的.net类库称为测试程序集。

今天讨论的桌面程序图像渲染性能测试主要应用于以下两种应用:

  1. 普通PC桌面WPF应用(分辨率<2K)
  2. 大屏幕可视化WPF应用(分辨率>8K)

4.1、普通PC桌面WPF应用

首先,回到之前的两个问题:
怎么检查图像渲染的质量?
怎么获取图像渲染的响应时间?

首先将正常渲染完的控件输出成图片

// 将控件uiElement输出到图片aa.bmp
uiElement.CaptureBitmap(@"D:\aa.bmp");

使用测试工具驱动启动被测应用并开始计时,在渲染过程中快速截图,实时比较两幅图片是否完全相等,如果相等并结束计时并写入响应时间。

// 比较两幅图片是否完全相同(所有像素点都相同)
bool isEqual = ImageHelper.IsEqual(@"D:\image1.bmp", @"D:\image2.bmp");

判断两幅图是否完全相同

/// <summary>
/// 判断两幅图是否完全相同
/// </summary>
/// <param name="imageFile1">待比较的第一幅图</param>
/// <param name="imageFile2">待比较的第二幅图</param>
/// <returns>如果两幅图完全相等,则返回true,否则返回false</returns>
public static bool IsEqual(string imageFile1, string imageFile2)
{
    
  float similarity = Compare(imageFile1, imageFile2);
  return similarity == 1;
}

影响图片输出的因素:
1.显卡,不同显卡输出文字和渐变色的时候有细微的差别,所以不同机器上显示的控件和输出的图片通常不完全相同,特别是当控件上有文字的时候。
2.DPI设置,将机器的DPI设置为120%时,100x100大小的控件将显示为120x120像素
3.当在远程桌面上运行测试时,远程连接的选项“字体平滑”会影响控件显示和输出的图片

4.2、大屏幕可视化WPF应用

由于大屏幕的分辨率8K起步,也就不适应上面的截图判断方法了,为什么呢?
我们简单来计算8K图片的大小吧

分 辨 率 : 7680 × 4320 = 33177600 像 素 ≈ 95 M B 分辨率:7680×4320=33177600像素≈95MB 7680×4320=3317760095MB

我们常见显示器用256种状态标识屏幕上某种颜色的灰度,而屏幕采用三基色红绿蓝(RGB),不压缩的情况下一个像素需要占用24bit(3字节),这个就是常说的24位真彩色。
近100MB的图片实时截图并进行判断,本身两个动作就会对机器的计算资源消耗巨大,会严重影响性能测试准确性。

这里我们折中使用实时判断标志位RGB像素点的方法来判断图片渲染的结果
首先,我们会使用取色器采样几个最后图像渲染完成的坐标像素点RGB值
原理其实很简单,只需要两步:

  1. 鼠标移动的时候获取鼠标光标的位置
  2. 鼠标单击获取当前鼠标光标的位置的RGB颜色值到粘贴板

涉及HookManager技术

namespace GetColor
{
    
   public partial class Form1 : Form
   {
    
       //显示设备上下文环境的句柄
       private IntPtr hdc = IntPtr.Zero;
       private int maxY, maxX;
       public Form1()
       {
    
           InitializeComponent();
           this.BackColor = Color.Brown;
           this.TransparencyKey = Color.Brown;
           this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
           this.WindowState = FormWindowState.Maximized;
           this.TopMost = true;
           this.Cursor = Cursors.Cross;
           HookManager.MouseMove += OnMouseMove;
           this.KeyPress += OnKeyPress;
           this.MouseClick += OnMouseDown;
           Size screenSize = Screen.PrimaryScreen.Bounds.Size;
           this.maxX = screenSize.Width - this.label1.Width;
           this.maxY = screenSize.Height - this.label1.Height;
           this.SetLabel(Control.MousePosition.X, Control.MousePosition.Y);
       }
       //鼠标移动,实时获取鼠标位置
       private void OnMouseMove(object sender, MouseEventArgs e)
       {
    
            this.SetLabel(Control.MousePosition.X, Control.MousePosition.Y);
       }
       private void SetLabel(int x, int y)
       {
    
           if(this.hdc == IntPtr.Zero)
           {
    
               this.hdc = GetDC(IntPtr.Zero);
               this.label1.Location = new Point(Math.Min(this.maxX, x + 10), Math.Min(this.maxY, y + 10));
               int color = GetPixel(this.hdc, x, y);
               this.label1.Text = string.Format("X={0},Y={1}\r\nColor:#{2}\r\n{3}", x, y, Convert.ToString(color, 16).PadLeft(6, '0'), color);
               this.Update();
               ReleaseDC(IntPtr.Zero, this.hdc);
               DeleteDC(this.hdc);
               this.hdc = IntPtr.Zero;
           }
       }
       private void OnKeyPress(object sender, KeyPressEventArgs e)
       {
    
           if (e.KeyChar == (char)Keys.Escape)
           {
    
               UnHook();
               Application.Exit();
           } 
       }
       private void OnMouseDown(object sender, MouseEventArgs e)
       {
    
           //检索一指定窗口的客户区或整个屏幕的显示设备上下文环境的句柄
           this.hdc = GetDC(IntPtr.Zero);
           //指定坐标点的像素的RGB颜色值。
           int color = GetPixel(this.hdc, e.X, e.Y);
           //鼠标单击拷贝值
           if (e.Button == MouseButtons.Left)
           {
    
               Clipboard.SetText(string.Format("<Point x=\"{0}\" y=\"{1}\" color=\"{2}\"/>", e.X, e.Y, color));
           }
           ReleaseDC(IntPtr.Zero, this.hdc);
           DeleteDC(this.hdc);
           UnHook();
           Application.Exit();
       }
       private void UnHook()
       {
    
           HookManager.MouseMove -= OnMouseMove;
       }
       [DllImport("user32")]
       public static extern IntPtr GetDC(IntPtr hwnd);
       [DllImport("gdi32")]
       public static extern int GetPixel(IntPtr hdc, int x, int y);
       [DllImport("user32.dll")]
       public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);
       [DllImport("gdi32.dll")]
       public static extern IntPtr DeleteDC(IntPtr hDc);
   }
}

小程序截图:
采集像素RGB工具

把图像渲染结果采样点填入测试工具的XML配置文件后,我们使用测试工具启动程序开始计时并实判断采样标志位像素点的RGB值,如果全部通过结束计时并写入渲染响应时间

   public void ValidateStage(Screen screen)
        {
                
            bool allReady = false;
            foreach (var stage in screen.ValidationStages)
            {
    
                stage.IsReady = false;
            }

            DateTime now = DateTime.Now;
            while (!allReady && (DateTime.Now - now).TotalSeconds < this.Config.Timeout)
            {
    
                allReady = true;
                foreach (var stage in screen.ValidationStages)
                {
    
                    if(!stage.IsReady)
                    {
    
                        stage.IsReady = this.ValidatePointsOneTime(stage.ValidatePoints);

                        if (stage.IsReady)
                        {
    
                            TimeSpan cost = DateTime.Now - now;
                            this.logger.LogInfo(string.Format("{0}耗时{1}", stage.Name, cost.TotalSeconds));
                            this.logData += string.Format(",{0}", cost.TotalSeconds);
                        }
                        else
                        {
    
                            allReady = false;
                        }
                    }                    
                }

                Thread.Sleep(this.Config.TryInterval);
            }

            foreach (var stage in screen.ValidationStages)
            {
    
                if(!stage.IsReady)
                {
    
                    foreach (ValidatePoint point in stage.ValidatePoints)
                    {
    
                        int color = Root.GetPointColor(point.X, point.Y);
                        this.logger.LogInfo(string.Format("点({0}, {1})的颜色为{2}", point.X, point.Y, color));
                    }

                    this.logger.LogInfo(string.Format("{0}秒内{1}未绘制完", this.Config.Timeout.ToString(), stage.Name));
                    this.logData += string.Format(",>{0}", this.Config.Timeout);
                }
            }
        }

        public void ValidateStage(Screen screen, ValidationStage stage)
        {
    
            DateTime now = DateTime.Now;

            bool screenReady = this.ValidatePoints(stage.ValidatePoints);
            if (screenReady)
            {
    
                TimeSpan cost = DateTime.Now - now;
                this.logger.LogInfo(string.Format("{0}耗时{1}", stage.Name, cost.TotalSeconds));
                this.logData += string.Format(",{0}", cost.TotalSeconds);
            }
            else
            {
    
                this.logger.LogInfo(string.Format("{0}秒内{1}未绘制完", this.Config.Timeout.ToString(), stage.Name));
                this.logData += string.Format(",>{0}", this.Config.Timeout);
            }
        }

        public bool ValidatePointsOneTime(List<ValidatePoint> validationPoints)
        {
    
            foreach (ValidatePoint point in validationPoints)
            {
    
                int color = Root.GetPointColor(point.X, point.Y);
                if (!Common.IsSimilarColor(color, point.Color))
                {
    
                    return false;
                }
            }

            return true;
        }

        public bool ValidatePoints(List<ValidatePoint> validationPoints)
        {
    
            DateTime endTime = DateTime.Now + TimeSpan.FromSeconds(this.Config.Timeout);
            bool finished = false;
            while (DateTime.Now < endTime)
            {
    
                bool allPointReady = true;
                foreach (ValidatePoint point in validationPoints)
                {
    
                    int color = Root.GetPointColor(point.X, point.Y);
                    //this.logger.LogInfo(color.ToString() + "   " + point.Color.ToString());
                    if (!Common.IsSimilarColor(color, point.Color))
                    {
    
                        allPointReady = false;
                        break;
                    }
                }

                if (allPointReady)
                {
    
                    finished = true;
                    break;
                }
                else
                {
    
                    System.Threading.Thread.Sleep(this.Config.TryInterval);
                }
            }

            if(!finished)
            {
    
                foreach (ValidatePoint point in validationPoints)
                {
    
                    int color = Root.GetPointColor(point.X, point.Y);
                    this.logger.LogInfo(string.Format("点({0}, {1})的颜色为{2}", point.X, point.Y, color));
                }                
            }

            return finished;
        } 
        

实际效果:
测试结果

参考资料:

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zuozewei/article/details/82656926

智能推荐

Java8 parallelStream——共享线程池对性能解析_jdk8 parallelstream 性能-程序员宅基地

文章浏览阅读3.3k次,点赞2次,收藏8次。最近做压测中发现一个应用中cpu过高,导致接口超时rt情况有些不大稳定,jstack打印线程一直在parallelStream相关的代码出进行计算。故对parallelStream相关做一下研究,找一下优化方法。java8并行流parallelStream,相信很多人都喜欢用,特别方便简单。但是有多少人真正知道里面采用的共享线程池对密集型任务,高并发下的性能影响呢可能你的一个应用里面..._jdk8 parallelstream 性能

C++ 11 创建和使用 unique_ptr_unique_ptr创建空节点-程序员宅基地

文章浏览阅读292次。https://www.cnblogs.com/DswCnblog/p/5628195.htmlunique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr。这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。我们建议..._unique_ptr创建空节点

NetCoreAPI配置全局路由_selector.attributeroutemodel.template-程序员宅基地

文章浏览阅读853次。1:新增类:RouteConvention,继承自IApplicationModelConvention/// <summary> /// 全局路由前缀配置 /// </summary> public class RouteConvention : IApplicationModelConvention { /// <summary> /// 定义一个路由前缀变量 /// </su_selector.attributeroutemodel.template

算 数-程序员宅基地

文章浏览阅读64次。从woody那里copy一段最简的fib代码[code="ruby"]x,y = 0,1 Array.new(10) {|i| [0,1].include?(i) ? 1 : (x,y = y,x+y)&&(x+y) } [/code]生成了这么多,太多了,中途终止了,不知道多少条。[code="ruby"] 1, 1, 2, ..._359579325206583560961765665172189099052367214309267232255589801

Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!-程序员宅基地

文章浏览阅读280次。本文原题“从实践角度重新理解BIO和NIO”,原文由Object分享,为了更好的内容表现力,收录时有改动。1、引言这段时间自己在看一些Java中BIO和NIO之类的东西,也看了很多博客,发现各种关于NIO的理论概念说的天花乱坠头头是道,可以说是非常的完整,但是整个看下来之后,发现自己对NIO还是一知半解、一脸蒙逼的状态(请原谅我太笨)。基于以上原因,..._java bio粘包处理

Python3.9环境搭建RobotFramework_python-3.9.9-amd64用那个版本ride-程序员宅基地

文章浏览阅读9k次,点赞2次,收藏12次。Robot Framework是一个基于Python的,可扩展的关键字驱动的测试自动化框架,用于端到端验收测试和验收测试驱动开发(ATDD)。_python-3.9.9-amd64用那个版本ride

随便推点

Hbase相关操作_hbase 查询-程序员宅基地

文章浏览阅读2.4k次。1.进入shellhbase(main):003:0>hbase shell2.查看所有表hbase(main):003:0> list3.根据rowKey查询某个记录hbase(main):003:0>get '表名','rowKey'4.常用过滤器过滤方式是通过value过滤,匹配出value含7259的数据。scan 'buss_surface', FILTER=>"ValueFilter(=,'substring:7259')"过滤方式是通_hbase 查询

噪声:Practical Poissonian-Gaussian noise modeling and fitting for single-image raw-data-程序员宅基地

文章浏览阅读2k次,点赞4次,收藏16次。Practical Poissonian-Gaussian noise modeling and fitting for single-image raw-data文章目录Practical Poissonian-Gaussian noise modeling and fitting for single-image raw-dataPoissonian-Gaussian ModelingThe Noise Profile AlgorithmWavelet domain analysisSegmentat_practical poissonian-gaussian noise modeling and fitting for single-image ra

计算机开机最快设置,w7提高开机速度如何操作_win7电脑怎么开机更快-程序员宅基地

文章浏览阅读4k次。由于win7电脑使用时间过长或者存放时间久了,难免会出现硬件各方面的老化或者堆积了大量的垃圾,因此就会导致电脑开机时的速度有所降低,对此有些用户就想,在不更换硬件的条件下,有没有方法能够提高一下开机速度,那么win7电脑提高开机速度如何操作呢?这里小编就来告诉大家win7电脑开机更快操作步骤。具体方法:1、在任意界面按下:windows键+R,然后在框内输入msconfig,点确定2、然后选择“启..._如何提高w7系统的开机速度

1688API接口:item_search - 按关键字搜索商品_1688 一件代发 api-程序员宅基地

文章浏览阅读672次。今天分享的是1688平台API,item_search - 按关键字搜索商品接口1688的API开发接口,我们需要做下面几件事情。1)开放平台注册开发者账号;2)然后为每个1688应用注册一个应用程序键(App Key) ;3)下载1688API的SDK并掌握基本的API基础知识和调用;4)利用SDK接口和对象,传入AppKey或者必要的时候获取并传入SessionKey来进行程序开发;5)利用1688平台的文档中心和API测试工具,对接口进行测试。从而了解返回信息,方便程序获取1688_1688 一件代发 api

vue-property-decorator使用指南_vue-property-decorator emit update-程序员宅基地

文章浏览阅读3.1k次,点赞2次,收藏12次。在Vue中使用TypeScript时,非常好用的一个库,使用装饰器来简化书写。一、安装npmi-Svue-property-decorator@Prop @PropSync @Provide @Model @Watch @Inject @Provide @Emit @Component(provided byvue-class-component) Mixins(the helper function namedmixinsprovided byvue-cla..._vue-property-decorator emit update

(七)用ChartDirector绘制实时图表-程序员宅基地

文章浏览阅读467次。本示例演示如何用Web图表控件 ChartDirector 绘制一个配置有刷新率的实时图表。在本例中,由一个计时器驱动的随机数生成器生成新的数据值,新产生的值会转换到数据数组中,然后显示在图表上。图表由一个秒表进行更新,这样图表的刷新率可独立于数据率。此外,这个图表支持暂停以方便用户查看,但是后台的数据仍然在继续更新。图表刷新计时器调用CChartViewer.update..._c++ chartdirect updateviewport