如何高效处理数百个项目的数千个C#文件以查找常量依赖
简介
我正在解决以下问题:
- 给定一个包含一个或多个公开可见常量的C#类型X,求解决方案中所有依赖类型X中常量的C#类型有哪些?
由于常量会在编译期内联,检查二进制文件没有意义,我们需要使用Roslyn API检查源代码。
我不打算使用语义分析,因为它的开销非常高。我会先使用正则表达式检查给定文件是否疑似使用了目标常量,再通过语法树验证。该方案并非万无一失,但足够好用且相对更快。
当前处理规模如下:
- 项目数量:333
- 文件数量:45280
- 存储介质:SSD硬盘
我的实现方案
整体流程如下:
- 生成相关
Microsoft.Build.Evaluation.Project对象流,从中获取C#文件列表 - 基于C#文件列表生成C#文件内容流
- 对每个C#文件内容匹配指定正则表达式,每个匹配项都通过对应C#文件内容的语法树判断是否确实使用了目标常量。如果确认则上报对应的类型。
我定义了几个小型辅助类型:
ProjectItem
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实例,该操作开销很高。
TextItem
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 Dataflow流水线:
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。 - 我认为当前流水线(通过默认TaskScheduler)使用的是ThreadPool线程,它是否应该使用真正的独立线程?比如通过
Task.Factory.StartNew(..., TaskCreationOptions.LongRunning);创建的线程?它目前使用的是「合适」的线程吗?如果不是该如何修复?我看到有建议实现自定义TaskScheduler,但我找不到示例,现有实现似乎依赖内部逻辑,不知道该如何处理。 - 我尝试过提高
ProjectItem和TextItem生成环节的MaxDegreeOfParallelism,因为这两个环节以IO为主,比最后检查C#文件内容的环节慢得多,但并没有带来明显的性能提升。我的认知是流水线中越慢的环节需要越高的并行度,但另一方面我不知道SSD读取可以支持多少并行度,完全不知道该如何profiling该流程。
内容的提问来源于stack exchange,提问作者mark
相关产品推荐
相关产品推荐

