Easy Profiler - C++ 的编译时分析器






4.94/5 (33投票s)
轻松检测代码,可视化、解释结果,跟踪优化,比较和决策。

目录
引言
本文提出了一种新的分析方法,它侧重于解释而不是详尽和复杂的结果。以易于使用的检测例程(主要由三个关键词表示:活动、任务、工作者)为代价,您将 C++ 应用程序进行检测并针对“收集器 DLL”进行编译,然后使用预设的分析比较工具轻松观察和解释结果。
更新
对于本文初始版本(11月6日)的读者,本次更新(11月13日)包含一个“扩展”用例,其中我将上个月 Grebnev 的“测试并发容器”文章代码作为真实案例来应用 Easy Profiler。新的 Misc.zip 包包含缺失的 CHM 帮助文件,以及 Observer 应用程序中使用的 Ribbon 命令图标的 PNG 源。“导出和自定义”子部分以及“限制和策略”部分中关于颜色系统问题的讨论。
背景
这个工具是在对现有分析解决方案的持续沮丧以及个人/工作项目急需大量分析的紧迫性中开发的。标准的“强大”分析器为我们提供了自动性能收集方案,使我们摆脱了手动插入代码的“所谓”负担。应用程序采样、测量代码注入等——在所有情况下,目标应用程序的性能都会受到显著影响,我们必须处理大量与每个函数/类调用等相关的分析数据。解释过于困难。我们只能检测到单一现象,但从未检测到“架构级缺陷”。超统计数据无法聚合。收集时节省的时间,在处理定量和细粒度输出数据时完全损失了。此举是对一个即时观察的后续行动:对这些自动分析器的任何改进,要么会面临过于具有挑战性的聚合案例而无法支持,要么会面临过于复杂的用户辅助(聚合/限制)UI 流程而无法避免同样的时间损失。因此,我们求助于编译时前提:您可以获得少量检测例程来轻松描述您的代码情况,并且可以获得预设的多种分析比较工具,以即时应用于由检测代码描述的一组功能,并以定性图表形式给出结果。
用法
这里介绍的工具包括两个组件
- 收集器:一个“纯”DLL,暴露检测例程。
- 观察器:一个 MFC 9.0 应用程序,用于“观察”和分析由收集器创建的包含性能测量值的 XML 文件。
要开始分析您的应用程序,您只需使用收集器 DLL 编译它,运行它,然后使用独立的观察器应用程序打开生成的 XML 文件。
因此,您会找到声明 IR 的头文件和用于编译过程的 Collector .lib 模块。将头文件添加到您的项目,添加 .lib,并将 DLL 部署在您的项目可执行文件旁边。
当然,您可以获得所有源代码,以便根据您的特定需求进行更改。请注意,在可下载的包中有一个 readme.txt 文件可供遵循。别忘了可以参考的 CHM 帮助文件。

收集器
如前所述,这个 DLL 包含 IR 的实现。首先,选择一个要分析的代码区域,命名它,并用以下两个调用包围它:被包围的代码可以是一个简单的调用,但它会导致复杂的场景。
startRegion(regionName, collectionType);
//
// The target code goes here. (may trigger external regions
// instrumented by measurement IRs)
//
flushRegion(regionName, outputDir, outputType)
当检测到 flushRegion
时,所有测量值都将写入单个文件。区域名称是帮助 Observer 在测试之间进行比较的签名。典型的场景是,您对代码进行检测,测试解释结果,决定进行特定更改,保留检测代码,然后再次进行测试,然后要求 Observer 对同一区域的两个“测试实例”执行比较工具。
区域测试可以存储在一个或多个单独的文件中。创建时间将被设置为测试的描述值。Observer 将帮助发现任何测试并使其可供您使用,无论您何时想要执行分析/比较操作,因为此类操作确实对同一区域测试的一组功能或属于同一区域的不同测试的匹配功能组进行操作

为了描述目标区域的代码架构,有三个“功能”可用
- 活动:用
startActivity(activityName)
和stopActivity(activityName)
包围子块:这个子块应该在区域执行生命周期中只执行一次。您可以使用insertCheckpoint(pointName, activityName)
描述活动中的点事件。 - 任务:用
startTask(taskName)
和stopTask(taskName)
包围它:这个子块旨在多次(在循环内)并发执行。 - 工作者:用
startWorker(workerName)
和stopWorker(workerName)
包围代码:它旨在模拟线程何时开始执行某些操作以及何时处于休眠状态。
你如何使用它?
好的。假设您有一段特别慢的代码,它调用了三个独立的函数。它是这样工作的
void SlowCode()
{
startRegion("my region", 'v')
//
startActivity("main")
func1();
insertCheckpoint("func1 finished", "main")
func2();
insertCheckpoint("func2 finished", "main")
func3();
stopActivity("main")
//
flushRegion("my region", "C:\tests", 's')
}
因此,这是一个名为“my region”的区域,其中包含一个类型为活动且名为“main
”的特征实例。在复杂情况下,您的代码可能会使用任务和工作者。
检查 IR 头文件,您可以发现包装直接调用 IR 的宏。这样,您可以通过取消定义特定符号来轻松消除检测例程的影响,从而避免删除源代码中的引用。
测试运行的结果是一个输出 XML 文件,其中包含测量值。例如

观察器
左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。
这是工具中负责打开测试数据并允许快速分析/比较操作的部分

其思想是打开测试数据,选择一个分析/比较操作及其“操作数”(区域的特征),点击按钮,即可立即看到图表。
左侧窗格(测试数据)包含从打开的 XML 文件读取的原始数据的树形视图。中心视图是一个图表控件。顶部功能区上的上下文选项卡有助于自定义图表(如 Excel)。主页选项卡用于打开和编辑测试数据。
分析/比较
“分析”选项卡用于选择区域测试、其中的特定功能以及选择操作按钮。

示例(共有 14 种分析场景)
- 您选择一个特定功能,然后显示其检查点的时间线视图。
- 您选择一个任务并显示其“运行”历史记录。
- 您选择一个功能,并以饼图形式显示每个任务对该功能执行的贡献。
因此,结果总是可视化图表。目标是缩短解释和决策的时间。无需导出值并求助于电子表格。
比较功能区选项卡和比较目标左侧面板协同工作。要执行任何比较操作,您自然需要选择同一区域的两个测试。观察者会帮助您发现所有已打开测试中所有可用的签名(区域名称)

因此,您所要做的就是选择区域名称,然后使用已勾选的树控件选择要比较的测试组,然后选择作为所需操作操作数的功能组。
在所有图表中,时间始终在 Y 轴上(无论是时间值还是时间持续时间值)。X 轴和数据系列根据分析/比较操作而不同。尝试使用您的逻辑来解读图表,否则请在文章论坛中留言。
导出和自定义
分析您的代码,可视化某个代码交互的性能后,您肯定会对自定义图表图形并将其传达给其他人感兴趣。
“图表工具”上下文功能区选项卡是您可以找到控制图表“外观”的命令的地方。我必须承认,它们不像常规的“上下文选项卡”那样起作用(常规= Fluent UI 参考中描述的)。
“设计”选项卡可帮助您更改图表的“设计”。例如,您可以修改图表类型或选择调色板。其他命令可以提供其他选项(3D、渲染质量)。
“布局”选项卡控制构成图表的各种元素的放置,即:图表标题、图例、数据标签、坐标轴和网格线。
最后,“格式”选项卡可能是您自定义图表最有趣的命令组。如果您已经使用过 Excel,那么理念是相同的。首先,使用“当前选择”功能区面板中的“选择项目”组合框选择要编辑其格式的项目。可以使用底部“更改文本”编辑框编辑该元素的文本。如果您不喜欢某个数据系列标题的自动名称,这可能很有用。如果该元素具有某种形状,那么您可以使用“形状填充”面板格式化该形状。如果它有轮廓,则有一个“轮廓”面板可以格式化颜色、样式和大小。如果它是文本,则有一个标准的“字体”面板供您使用。其他选项包括元素对齐、停靠等。
通过主功能区按钮菜单中的“另存为”按钮完成图表导出。选择目标文件路径并选择六种不同图像格式中的一种。
用例
现在到用例。您必须在这里帮助我,因为我不想复制粘贴我在网站上已经撰写过的长篇内容。我建议您阅读它。第一个链接甚至以虚构的有趣故事形式编写。我将仅限于在此处放置重要注释。
基本用例
这是一个基本案例。检测既是隔离过程,也是聚合过程。在大多数情况下,这意味着您是编写检测代码的人,或者至少您了解它。开发人员事先知道在哪里寻找现象原因。
在以下结果图中

Jeff 提出了一个重要的疑问:“…这真的很奇怪。如果不是因为数据加载的方式,那其他操作阶段是如何快速处理相同的数据流的?”
如果不重复组合一些度量,并以几次点击的便利性将它们定性可视化,那么要解决这样的问题并不容易。
高级用例
如您所见,检测例程用于描述服务器应用程序的代码架构。区域特征(活动+任务+工作者)反映了代码的骨架元素。因此,分析/比较工具用于揭示该架构的可疑元素之间的交互,从而从更高层次批评代码行为。

在上面的图表中,使用了一个特定的分析工具来提出问题:名为“解码”的任务对名为“worker1
”的线程的总体执行贡献了多少。因此,这是一个工作者和任务之间的简单交互。
这种方法不仅可以解决一些性能瓶颈,还可以帮助在进行昂贵的代码更改的风险之前验证一些决策。
某些概念也可以测量。思考活动-工作交互以及如何将其用于评估并行代码的实际收益。
扩展用例,基于 Grebnev 的并发容器测试
自从这篇文章发表以来,我总觉得缺少点什么。尽管我为用户提供了两个用例,但我强迫他面对一种特定的代码情况,甚至是演示性质(虚构)或未知代码。所以,没有什么比采用一个大多数读者都能理解的真实用例更好了。正是出于这个原因,我选择了一篇出现在上个月投票列表中的有趣文章,我将通过检测其中的代码来展示这个工具。
这篇文章是:“测试并发容器”;您应该不会错过阅读它,但由于我无法排除这种可能性,您可以相信以下段落将帮助您学习为实现我们的目标所必需了解的内容。
下载代码。您会找到一个“main”文件和其他包含并发容器不同实现的文件。容器是一个对象,我们可以在其中存储某些类型的其他元素对象。STL 的 map 对象是一个示例容器,但它不是并发的,因为您无法在没有可能影响数据甚至调用进程的问题的情况下同时插入。std::vector
也是如此。所以并发容器,简单地说,就是添加了同步的容器。然而,线程竞争和由此导致的线程阻塞会影响读/写访问操作的性能。因此,一个成功的并发容器是在不违反串行访问共享元素原则的情况下,最大限度地减少“阻塞”的容器。因此,文章代码就像一个四种方法的测试基准
- 英特尔 TBB
- CS 表格
- 自旋锁
- 锁池
名为 g_map
并在后续测试中使用的容器对象类型由宏控制,用于在编译时切换不同的实现
#ifdef TEST_CS_TABLE_CONTAINER
CST_map_t < labelong, test_container_element,LOCK_TYPE > g_map;
#elif
//etc. (in main.cpp line 76).
宏声明位于 stdafx.h 中
#define TEST_TBB_CONCURRENT_CONTAINER
//#define TEST_CS_TABLE_CONTAINER
//#define TEST_SPINLOCK_CONTAINER
//#define TEST_POOLLOCK_CONTAINER
容器暴露以下“接口”:插入/修改/读取/删除。三种不同类型的测试线程将“派生”自基参数化 Test_worker
类,并执行不同类型的操作。
Container_builder
(将在填充容器后执行插入和删除操作)Writer
(将执行修改操作:它随机选择一个访问,然后检索并修改相应的元素(如果找到))Reader
(将执行读取操作)
所以 main 函数所做的就是启动一个 Container_builder
线程实例,以及一组 Writer
和 Reader
,它们将同时运行,直到完成指定次数的迭代。然后测量平均读/写操作的持续时间,然后使用不同的容器类型再次进行测试,直到所有测试完成。作者为我们提供了以下图表,这肯定是通过借助于外部程序(如电子表格)并手动导出多次运行获得的测量结果。

Easy Profiler 旨在在此处介入,以减轻重新创建“测量代码”的负担(请参阅 test_iteration
方法、Performance_counter_meter
类及其背后的 QueryPerformanceCounter
)。需要手动将“测量值”导出到外部程序,在那里进行解释。如果当前实现以不同的方式失败,我相信这种方法是可行的。那么,让我们看看事情是如何完成的。
首先,我们采取必要的步骤将 Collector
模块集成到目标代码中。在“Deploy module”zip 文件中,您会找到 routines.h、collector.lib 和 collector.dll 文件。将父文件夹添加到您的项目包含路径 + 附加库路径,并在 stdafx.h 中 #include
routine.h。
用以下两个调用包围 main.cpp
_startRegion("containers test",'v')
_flushRegion("containers test","C:/profTests/",'s')
区域是一种将检测限制在仅属于这两个调用的调用的方式。其目的是能够将分析限制在可疑部分,从而消除由在同一测试运行期间执行的其他部分产生的“噪音”数据。这里的代码很小,无需“超级目标”区域或查看其实际用途。
现在您可以运行应用程序。一个新的 XML 文件已创建,但它不包含任何测量值。

如前所述,区域名称就像签名一样,将有助于 Observer 进行比较操作。假设开发人员首先检测其代码,可视化并解释结果,然后进行不改变检测代码的代码更改,然后开始观察差异。
在我们的情况下,导致代码更改的原因是并发容器的不同实现。在检测到位后,我们通过切换到特定的容器实现来运行不同的测试,然后打开结果文件并将描述从“默认时间日期”更改为容器方法本身的名称。

描述对于后续的比较操作非常重要。比较工具可通过“比较功能区选项卡命令”获得。首先,您需要确保所有区域测试都已打开,然后将焦点设置到所需的区域签名。

底部操作数组合框会自动填充第一个识别的区域测试的“功能”名称。同时,比较目标左侧面板填充了所有与同一指定区域名称匹配的区域测试。

对 startRegion
的调用将值 'v'(详细)提供给第二个参数。这反映在序列化时的 XML 属性 highperformance=false
中。这是为了禁用分割合并机制,该机制在性能非常高的代码可能生成千兆字节数据测量值的情况下被激活。通常,这里也是这种情况;然而,由于线程竞争是一种概率现象,我将继续使用 Grebev 已经使用的统计方法,但以另一种方式(请注意线程冲突是概率性的)
我们修改 main 函数,首先填充容器以启动构建器线程,使其持续运行而不限制迭代次数,然后启动 8 个读取器,等待它们完成,然后启动 4 个写入器和 4 个读取器,再次等待它们完成,然后启动 8 个写入器并等待它们完成,最后停止容器构建器。
在此过程中,容器构建器预计会引起全局范围锁定,因为插入和删除会影响容器结构本身。在最初的 8 个读取器期间,我们应该只担心容器。但是当我们启动 4 个读取器和 4 个写入器时,我们还会引起元素范围锁定。处理异构情况的能力应该通过检查与 8 个写入器情况的差异来评估。
以下是代码修改和检测代码的方式
int _tmain(int, _TCHAR*)
{
_startRegion("containers test",'v')
_startActivity("main")
Container_builder::setup_test_container();
Container_builder* pBuilder = new Container_builder();
HANDLE hBuilder = pBuilder->start(::rand());
_insertCheckPoint("container built","main")
HANDLE readerThreads[8];
//Reader* reader[8]; No need to store addresses.
for (int i = 0; i < 8; i++)
{
Reader* pReader = new Reader();
readerThreads[i] = pReader->start(::rand());
}
::WaitForMultipleObjects(sizeof(readerThreads)/sizeof(readerThreads[0]),
readerThreads, TRUE, INFINITE);
printf("8 readers\n");
_insertCheckPoint("8-readers","main")
HANDLE readerAndwriterThreads[8];
for (int i = 0; i < 4; i++)
{
Reader* pReader = new Reader();
readerAndwriterThreads[i] = pReader->start(::rand());
}
for (int i = 0; i < 4; i++)
{
Writer* pWriter = new Writer();
readerAndwriterThreads[4+i] = pWriter->start(::rand());
}
::WaitForMultipleObjects(sizeof(readerAndwriterThreads)/
sizeof(readerAndwriterThreads[0]), readerAndwriterThreads, TRUE, INFINITE);
printf("4 readers 4 writers\n");
_insertCheckPoint("4-readers 4-writers","main")
HANDLE writerThreads[8];
for (int i = 0; i < 8; i++)
{
Writer* pWriter = new Writer();
writerThreads[i] = pWriter->start(::rand());
}
::WaitForMultipleObjects(sizeof(writerThreads)/sizeof(writerThreads[0]),
writerThreads, TRUE, INFINITE);
printf("8 writers\n");
_insertCheckPoint("8-writers","main")
_stopActivity("main")
_flushRegion("containers test","C:/profTests/",'s')
return 0;
}
现在为不同的容器实现创建测试。每次取消注释 4 个容器宏符号中的一个,编译,然后运行。如果您没有我幸运,那么您应该能够测试所有 4 种实现;然而,由于我只有 Windows XP,并且显然其中一种实现依赖于新的 API 函数,因此以下结果仅对应于三种实现。
要一起打开所有测试,请打开 Observer,然后从“主页”选项卡按下“测试套件”命令。选择“C:/profTests”文件夹。激活“比较”选项卡和“比较目标”面板。选中已勾选树控件中的所有项目,从焦点组合框中选择“容器测试”区域和“主”功能。从单个活动面板中按下“时间线”命令。让我们为图表取一个好标题。激活“格式”选项卡,选择“图表标题”元素,然后在底部编辑框中输入文本,然后点击“确定”。

这是我们的图表

对于此基本比较命令(活动时间线比较),区域测试描述会自动用作数据系列的标题。
让我们选择另一个简单的命令(比较差异事件)

我们能从中推断出什么?首先,向 Intel 的 TBB 致敬。除此之外,还有许多其他结论;就我个人而言,我更感兴趣的是,其他两种实现给出的结果与直观预期相悖,即从 8 个写入者情况到 4 个读取者+4 个写入者情况,再到 8 个读取者情况,结果却不如预期。然而,我们的目标已经达到,如果您愿意,我们将在论坛中讨论解释:其思想是,如果您选择使用 Easy Profiler 来分析您的代码,那么您不仅避免了浪费时间重新发明“测量代码”,而且使分析/可视化/比较结果变得轻松快捷,从而达到解释并发现有关代码性能的意外事实。在这个最终场景中,我们只使用了 (14+12) 个分析/比较命令中的 2 个,但我敢挑战您是否可以使用自动分析器获得这样的结果图表。
源代码解释
你想修改代码吗?这里有一些指导。首先用 Visual Studio C++ 2008 打开解决方案。

Observer 项目对应于观察器应用程序。它依赖于一个 .NET 图表控件(参见“参考文献和依赖关系”部分),该控件被包装在 Chart Provider 项目中。CRLAdapter
是非托管 Observer 项目和 .NET Chart Provider 项目之间的桥梁。此外,它还依赖于 Collector 项目。因此,后者必须部署在 Observer 目录和您的检测应用程序可执行文件目录中。

收集器
这个 DLL 大量基于 STL 集合。当然,测量值会保存在 RAM 中,直到写入磁盘。XML-DOM 解析器用于协助序列化(读/写)。CFeature
类是所有三个特征的基类:CActivity
、CTask
和 CWorker
。CTest
表示一个“region
”,而 CTestsFile
是存储在一个文件中的一组测试。这里一切都应该不难。时间测量依赖于 QueryPerformanceCounter
,参考时间是调用 startRegion
。当 startRegion
的 collectionType
参数设置为值 'h
' 时,CTask
和 CWorker
中会发生分割合并机制。这允许对否则会生成千兆字节测量值的“高性能”代码进行分析!例如,当您想研究一个国际象棋引擎的行为时,它每秒执行数百万次位置计算。数据压缩只会导致一些分析/比较操作变得不可用(例如:任务执行历史)。
观察器
这个 MFC 应用程序依赖于 Collector DLL 导出的 public class
es 来执行序列化/计算过程。当您打开一个测试文件时,区域测试会呈现在左侧的树形窗格中
class CProfileTree : public CXHtmlTree
{
public : void renderFile(CTestsFile* pFile);//
}
pFile
已由 CTestsFile
中声明的静态函数创建,并包含所有子区域测试及其特征。
这些数据结构随后由“操作类”进行操作。当您选择一组操作数特征并单击操作按钮时,操作类工厂会创建一个操作类,传入操作数,并调用虚拟的 execute
成员。
class CAnalysisTool
{
public:
virtual void execute(CFeature* pTarget,CFeature* pExtraTarget=NULL)=0;
}
//
class CCompareTool
{
public:
virtual void execute(std::vector< CTest* > tests,
std::string targetName,std::string extraTargetName)=0;
}
如果是比较操作,则测试向量由已选树控件中的项目填充。当然,每个操作都有自己的逻辑来计算结果。例如
void CCompareTaskContributionToActivity::execute
( std::vector<CTEST*> tests,std::string targetName,std::string extraTargetName )
{
std::vector< CTest* >::iterator myIt;
chartControl.resetSeries();
int i=0;
for (myIt=tests.begin();myIt!=tests.end();myIt++)
{
i++;
CTest* pTest=*myIt;
//
CActivity* pActivity=(CActivity*) pTest->getFeatureByName(targetName);
CTask* pTask=(CTask*) pTest->getFeatureByName(extraTargetName);
//
std::string activitySeries=pTest->getDescription()+
std::string(" ")+pActivity->getName();
std::string taskSeries=pTest->getDescription()+
std::string(" ")+pTask->getName();
chartControl.addSeries(activitySeries, CChartControl::StackedColumn);
chartControl.addSeries(taskSeries, CChartControl::StackedColumn);
CString strProperty;
strProperty.Format(_T("StackedGroupName=%d"),i);
//
chartControl.setCustomProperty(activitySeries,strProperty);
chartControl.setCustomProperty(taskSeries,strProperty);
//
CActivity::PointsVector pointVector=pActivity->getPoints();
CActivity::PointsVector::iterator myIt;
myIt=pointVector.begin();
if (myIt==pointVector.end())
return;
CActivity::CCheckPoint* pLastPoint=*myIt;
myIt++;
while (myIt != pointVector.end())
{
CActivity::CCheckPoint* pCurPoint=*myIt;
double taskContribution=pTask->measureTimeContribution
(pLastPoint->getTime(),
pCurPoint->getTime());
chartControl.addPoint(activitySeries,pCurPoint->getName(),
pCurPoint->getTime()-pLastPoint->
getTime()-taskContribution);
chartControl.addPoint(taskSeries,pCurPoint->getName(),
taskContribution);
pLastPoint=pCurPoint;
myIt++;
}
}
}
图表可视化是通过 Microsoft .NET Framework 的 Microsoft Chart Controls(基于 Dundas Chart)完成的。因此,代码中我必须允许这个 .NET 控件集成到非托管 MFC 应用程序中的部分是比较糟糕的。
限制和策略
存在许多缺陷和潜在的改进。我在 2009 年第一季度开发它时,也是我创建第一个网站的机会。我在两个个人项目(服务器框架 + 国际象棋引擎)中使用了/继续使用它,然后我用它来帮助优化工作中的文档创建引擎。在这段时间里,根据网站统计,很少有人感兴趣。然后,我在我公司的工作室部分发布了它。没有什么改变。似乎今天很少有人编写极其缓慢的代码。:)(或者可能很少有人处理对性能要求高的项目。)
说真的,我希望将这个工具发布到这个伟大的平台,并将其开源会改变现状。我一个人无法完全投入其中:我已经用它解决了我的问题,并将继续这样做。现在需要多方携手合作,共同实现所需的更改和改进
-
数据预计算
修改所有 AnalysisTool 和 CompareTool 派生类,使其在任何渲染调用之前预先计算所有数据。这样做的好处是我们可以持久化数据值并以不同格式提供其导出。还要考虑图表生成性能,当所有数据计算完毕后才启用时。将计算和渲染代码分离的意图在此处可见,可惜被忽视了。
class CAnalysisTool { public: CAnalysisTool(void); ~CAnalysisTool(void); public: virtual void execute(CFeature* pTarget,CFeature* pExtraTarget=NULL)=0; virtual void render()=0; //..etc.
-
远程实时分析
想象一下这样一种情况:预先选择了一个分析工具,并且目标检测应用程序在远程主机上运行,并且图表随着从远程收集器实例接收到的传入数据流而更新:观察器将像一个“通用”仪表板一样运行!最需要的是收集器和观察器之间数据值的异步、流式传输机制。
-
颜色系统
有些分析/比较操作会生成非常复杂的图表,其中颜色对其可读性有很大影响。希望能添加一种机制,根据蓝色表示活动、红色表示任务、绿色表示工作者的规则,为每个数据系列选择颜色。即使在某些简单的“图表”中,最好也遵循此规则。例如,当选择“所有任务”面板下可用的“总时间”分析命令时,我们目前会得到一个类似于
这应该通过为每个数据饼图选择“红色”来更改。如果我们将红色的 RGB 值转换为 YCrCb 系统(例如),然后只选择在 Y 通道中不同的 5 个值,并将其转换回 RGB 颜色空间,这是可能的。
让我们看另一个问题。以下图表是通过在“单活动”面板下使用名为“堆叠”的分析工具,并选择“生命周期”活动 + “路由文本聊天”任务作为操作数而获得的。
请参阅 CHM 帮助文件,了解其背后的数学公式。这里也存在与颜色相同的先前问题:“路由文本聊天”系列需要使用红色。然而,红色必须是透明的,以显示其下方的蓝色;原因可以在公式本身中找到。
至于下面的图表(通过比较命令获得,显示了工作者和任务之间交互的演变)
左边的矩形应该自动填充绿色。第三个也一样。至于第二个和第三个,它们应该是红色的。然而,前两个必须具有相同的 Y 通道值,与后两个矩形不同。
最后,我们遇到了一个大问题
你认为你有一个解决方案,如何?我们有两个相同区域的测试,包含两个工作者,这里显示了这两个测试的总持续时间。坦率地说,我无法解决它,除非我们可以利用样式哈希或填充渐变选项,尽管不清楚如何保证我们能够自动且理论上获得不同的样式。
-
Bug
错误无处不在:例如,滥用 IRs 会系统地导致应用程序崩溃 :)(例如,在未提供给
insertActivity
调用的活动名称上调用insertCheckPoint
)。
热烈欢迎贡献者:Google 上的项目地点 现已就绪。
参考文献和依赖
这是第三方代码/控件列表
- Hans Dietrich 的 XHTMLTree
- Robert A. T. Káldy 的 Generic Factory
- 微软的 Microsoft Chart Controls for .NET Framework 3.5(Observer 程序运行必需) (下载链接)
- MFC 和 WinForms 集成:使图表控件能够托管在 MFC 界面中的思想和源代码。
- Microsoft .NET Framework 3.5(Observer 正常工作必需,因为它依赖于 .NET 图表控件)
- Frank Vanden Berghen 的 XMLParser
- 微软的 Visual C++ 2008 Feature Pack。这对于编译 Observer 至关重要。如果您有 VS SP1,我想您应该已经拥有它。
历史
这是第一个版本,我们称之为 0.1 alpha。由于应用程序复杂,需要 SVN。因此,有关更新,请访问 Google 项目页面。