【发布时间】:2021-11-07 09:37:47
【问题描述】:
简介
我正在解决以下问题:
- 给定具有一个或多个公开可见常量的 C# 类型 X 解决方案中依赖于 X 类型中的常量的所有 C# 类型是什么?
因为常量是在编译时内联的,所以检查二进制文件是没有意义的。我们需要使用 Roslyn API 检查源代码。
我不会使用语义分析,因为它会非常昂贵。相反,我将使用正则表达式来检查给定文件是否似乎使用了常量,然后使用语法树进行验证。不是防弹的,但足够好并且相对快。
目前的统计数据是:
- 项目数:333
- 文件数:45280
- SSD 驱动器
我的实现
整体方案是:
- 生成相关
Microsoft.Build.Evaluation.Project对象的流,这将为我们提供 C# 文件列表 - 从 C# 文件生成 C# 文件内容流
- 对于每个 C# 文件内容匹配给定的正则表达式,并为每个匹配确定是否使用该 C# 文件内容的语法树使用相关常量。如果是肯定的 - 报告相应的类型。
我不想过多的细节,另一方面我想提供足够的细节来解释我的困难。不多,请多包涵。
我使用了几个小的辅助类型:
项目项目
private class ProjectItem
{
public readonly string AssemblyName;
public readonly CSharpParseOptions ParseOptions;
public readonly IEnumerable<string> CSFilePaths;
public ProjectItem(TypeMap typeMap, string asmName)
{
AssemblyName = asmName;
var asmProps = typeMap.Assemblies[asmName];
ParseOptions = asmProps.GetParseOptions();
CSFilePaths = new Project(asmProps.ProjectPath).GetItems("Compile").Select(item => item.GetMetadataValue("FullPath"));
}
public IEnumerable<TextItem> YieldTextItems() => CSFilePaths.Select(csFilePath => new TextItem(this, csFilePath, File.ReadAllText(csFilePath)));
}
TypeMap 是我们解决方案中使用的所有类型和程序集的注册表。其他一些代码之前已经构建了它。将其视为可以回答某些问题的预言机,例如“给我给定程序集的解析选项(或项目路径)”。但它没有指定项目使用的 C# 文件列表。为此,我们需要实例化相应的 Microsoft.Build.Evaluation.Project 实例。哪个很贵。
文本项
private class TextItem
{
public readonly string AssemblyName;
public readonly CSharpParseOptions ParseOptions;
public readonly string CSFilePath;
public readonly string Text;
public TextItem(ProjectItem item, string csFilePath, string text)
{
AssemblyName = item.AssemblyName;
ParseOptions = item.ParseOptions;
CSFilePath = csFilePath;
Text = text;
}
public IEnumerable<TypeDefKey> YieldDependentTypes(TypeMap typeMap, TypeDefKey constTypeDefKey, Regex regex)
{
...
SyntaxTree syntaxTree = null;
foreach (Match m in regex.Matches(Text))
{
if (syntaxTree == null)
{
syntaxTree = CSharpSyntaxTree.ParseText(Text, ParseOptions, CSFilePath);
...
}
...
if (IsTheRegexMatchIndeedCorrespondsToTheGivenConstantType(syntaxTree, ...))
{
var typeDefKey = GetTheType(syntaxTree, ...);
yield return typeDefKey;
}
}
}
}
鉴于上述类型,我想出了这个简单的 TPL 数据流管道:
var regex = GetRegex(...);
var dependentAssemblies = GetDependentAssemblies(...);
var dependentTypes = new ConcurrentDictionary<TypeDefKey, object>();
var produceCSFilePaths = new TransformManyBlock<ICollection<string>, ProjectItem>(asmNames => asmNames.Select(asmName => new ProjectItem(typeMap, asmName)));
var produceCSFileText = new TransformManyBlock<ProjectItem, TextItem>(p => p.YieldTextItems());
var produceDependentTypes = new TransformManyBlock<TextItem, TypeDefKey>(t => t.YieldDependentTypes(typeMap, constTypeDefKey, regex));
var getDependentTypes = new ActionBlock<TypeDefKey>(typeDefKey => dependentTypes.TryAdd(typeDefKey, null));
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
produceCSFilePaths.LinkTo(produceCSFileText, linkOptions);
produceCSFileText.LinkTo(produceDependentTypes, linkOptions);
produceDependentTypes.LinkTo(getDependentTypes, linkOptions);
produceCSFilePaths.Post(dependentAssemblies);
produceCSFilePaths.Complete();
getDependentTypes.Completion.Wait();
问题和疑问
- 速度很慢 - 大约需要 50 秒,CPU 利用率低。我意识到这里有很多 IO,但仍然需要 CPU 来应用正则表达式并将内容解析到相应的语法树中。
- 我不明白如何将
TransformManyBlock与异步 IO 一起使用。ProjectItem.YieldTextItems函数可以返回IObservable<TextItem>或IAsyncEnumerable<TextItem>,但TransformManyBlock不会识别它。我是 TPL Dataflow 的新手,所以我不清楚如何解决这个问题。这就是为什么我使用阻塞File.ReadAllText而不是File.ReadAllTextAsync。 - 我认为我的管道使用 ThreadPool 线程(通过默认的 TaskScheduler),但它不应该使用真正的线程吗?喜欢用
Task.Factory.StartNew(..., TaskCreationOptions.LongRunning);创建的那些?那么,它是否使用“正确的”线程?如果没有 - 如何解决?我已经看到了实施自定义TaskScheduler的建议,但我找不到示例。现有的似乎依赖于内部实现,所以不清楚如何进行。 - 我尝试为
ProjectItem和TextItem生产增加MaxDegreeOfParallelism,因为这两个主要是IO,因此比最后一部分慢得多- 检查C# 文件内容。但它并没有在性能上产生太大的改进。据我了解,管道越慢,并行性就应该越高。另一方面,我不知道从 SSD 读取时可以有多少并行度。完全不清楚如何对其进行分析。
【问题讨论】:
-
看来这个问题太大太复杂了。最好把它分成小块,分别调查,问不同的问题
-
仅供参考,向
TransformManyBlock添加对IAsyncEnumerable<T>的支持是微软的计划,但团队无法及时完成.NET 6,因此it has been postponed for .NET 7。目前,您可能会通过查看this 的答案获得一些关于如何超越此限制的想法。 -
@TheodorZoulias - 我想这个想法不是将
produceCSFileText链接到produceDependentTypes,而是让前者使用SendAsync方法发送TextItem对象。然后手动处理两者之间的延续传播。
标签: c# multithreading asynchronous task-parallel-library tpl-dataflow