├── .gitignore ├── LICENSE.md ├── MSBuildFlameGraph.sln ├── README.md ├── readme-samples ├── blank-project.png ├── bt-plus-time-plus.png ├── d1reportTime.png ├── random-ascii-parallel.png └── ui-screenshot.png └── src ├── BuildTimeline ├── BuildData.cs ├── BuildTimeline.csproj ├── Entries │ ├── BuildEntry.cs │ ├── TimelineBuildEntry.cs │ └── TimelineEntry.cs ├── Events │ ├── BuildFinishedEvent.cs │ ├── BuildStartedEvent.cs │ ├── Context │ │ ├── EventContext.cs │ │ ├── MessageEventContext.cs │ │ ├── ProjectEventContext.cs │ │ ├── TargetEventContext.cs │ │ └── TaskEventContext.cs │ ├── ErrorEvent.cs │ ├── Event.cs │ ├── MessageEvent.cs │ ├── ProjectFinishedEvent.cs │ ├── ProjectStartedEvent.cs │ ├── TargetFinishedEvent.cs │ ├── TargetStartedEvent.cs │ ├── TaskFinishedEvent.cs │ ├── TaskStartedEvent.cs │ └── WarningEvent.cs ├── Properties │ └── AssemblyInfo.cs └── Timeline │ ├── Builder │ ├── PostProcess │ │ └── TimelineEntryPostProcessor.cs │ └── TimelineBuilder.cs │ ├── ThreadAffinity.cs │ └── Timeline.cs ├── Builder ├── App.config ├── App.xaml ├── App.xaml.cs ├── Builder.csproj ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── Resources │ ├── ExtraFlags_BtPlus.props │ ├── ExtraFlags_BtPlus_D1ReportTime.props │ ├── ExtraFlags_BtPlus_TimePlus.props │ ├── ExtraFlags_BtPlus_TimePlus_D1ReportTime.props │ └── ExtraFlags_TimePlus.props └── ViewModel │ ├── BuildConfigurationViewModel.cs │ ├── BuilderViewModel.cs │ └── Commands.cs ├── MSBuildWrapper ├── BuildEventConverter │ └── EventConverter.cs ├── BuildLoggers │ ├── AllMessagesLogger.cs │ ├── AllMessagesToCallbackLogger.cs │ ├── AllMessagesToCallbackRawLogger.cs │ └── AllMessagesToCallbackUILogger.cs ├── BuildMessage │ └── BuildMessage.cs ├── Compilation │ ├── Compilation.cs │ ├── CompilationResult.cs │ └── CompilationStatus.cs ├── MSBuildWrapper.csproj ├── Properties │ └── AssemblyInfo.cs └── SolutionCompiler │ ├── DataExtractor │ ├── CallbackPerMessageDataExtractor.cs │ ├── CompilationDataExtractor.cs │ └── CustomEventFormatExtractor.cs │ └── SolutionCompiler.cs ├── Model ├── BuildConfiguration │ └── BuildConfiguration.cs ├── BuildEventConverter │ └── Converter.cs ├── DiskFile │ └── DiskFile.cs ├── Model.csproj ├── Properties │ └── AssemblyInfo.cs ├── PropertyChangeNotifier │ └── PropertyChangeNotifier.cs ├── Solution │ ├── Project.cs │ ├── Solution.cs │ └── SolutionLoader.cs └── packages.config └── TimelineSerializer ├── ChromeTracing ├── ChromeTracingEvent.cs └── ChromeTracingSerializer.cs ├── Properties └── AssemblyInfo.cs └── TimelineSerializer.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | # ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true 235 | **/wwwroot/lib/ 236 | 237 | # RIA/Silverlight projects 238 | Generated_Code/ 239 | 240 | # Backup & report files from converting an old project file 241 | # to a newer Visual Studio version. Backup files are not needed, 242 | # because we have git ;-) 243 | _UpgradeReport_Files/ 244 | Backup*/ 245 | UpgradeLog*.XML 246 | UpgradeLog*.htm 247 | ServiceFabricBackup/ 248 | *.rptproj.bak 249 | 250 | # SQL Server files 251 | *.mdf 252 | *.ldf 253 | *.ndf 254 | 255 | # Business Intelligence projects 256 | *.rdl.data 257 | *.bim.layout 258 | *.bim_*.settings 259 | *.rptproj.rsuser 260 | 261 | # Microsoft Fakes 262 | FakesAssemblies/ 263 | 264 | # GhostDoc plugin setting file 265 | *.GhostDoc.xml 266 | 267 | # Node.js Tools for Visual Studio 268 | .ntvs_analysis.dat 269 | node_modules/ 270 | 271 | # Visual Studio 6 build log 272 | *.plg 273 | 274 | # Visual Studio 6 workspace options file 275 | *.opt 276 | 277 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 278 | *.vbw 279 | 280 | # Visual Studio LightSwitch build output 281 | **/*.HTMLClient/GeneratedArtifacts 282 | **/*.DesktopClient/GeneratedArtifacts 283 | **/*.DesktopClient/ModelManifest.xml 284 | **/*.Server/GeneratedArtifacts 285 | **/*.Server/ModelManifest.xml 286 | _Pvt_Extensions 287 | 288 | # Paket dependency manager 289 | .paket/paket.exe 290 | paket-files/ 291 | 292 | # FAKE - F# Make 293 | .fake/ 294 | 295 | # JetBrains Rider 296 | .idea/ 297 | *.sln.iml 298 | 299 | # CodeRush personal settings 300 | .cr/personal 301 | 302 | # Python Tools for Visual Studio (PTVS) 303 | __pycache__/ 304 | *.pyc 305 | 306 | # Cake - Uncomment if you are using it 307 | # tools/** 308 | # !tools/packages.config 309 | 310 | # Tabs Studio 311 | *.tss 312 | 313 | # Telerik's JustMock configuration file 314 | *.jmconfig 315 | 316 | # BizTalk build output 317 | *.btp.cs 318 | *.btm.cs 319 | *.odx.cs 320 | *.xsd.cs 321 | 322 | # OpenCover UI analysis results 323 | OpenCover/ 324 | 325 | # Azure Stream Analytics local run output 326 | ASALocalRun/ 327 | 328 | # MSBuild Binary and Structured Log 329 | *.binlog 330 | 331 | # NVidia Nsight GPU debugger configuration file 332 | *.nvuser 333 | 334 | # MFractors (Xamarin productivity tool) working folder 335 | .mfractor/ 336 | 337 | # Local History for Visual Studio 338 | .localhistory/ 339 | 340 | # BeatPulse healthcheck temp database 341 | healthchecksdb -------------------------------------------------------------------------------- /MSBuildFlameGraph.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.960 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Builder", "src\Builder\Builder.csproj", "{D5941EB2-3273-472D-9EA1-1C25F5C572FC}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "src\Model\Model.csproj", "{C49C74FD-E1C2-4310-9190-9996177902CD}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildTimeline", "src\BuildTimeline\BuildTimeline.csproj", "{19317603-ECDA-4CB8-B781-619CF69F2979}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {C49C74FD-E1C2-4310-9190-9996177902CD} = {C49C74FD-E1C2-4310-9190-9996177902CD} 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimelineSerializer", "src\TimelineSerializer\TimelineSerializer.csproj", "{7D335775-8D76-45B4-A035-695C88EB0134}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSBuildWrapper", "src\MSBuildWrapper\MSBuildWrapper.csproj", "{C8AC65D0-EE58-4CC1-880C-F97D78DFF83F}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {D5941EB2-3273-472D-9EA1-1C25F5C572FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {D5941EB2-3273-472D-9EA1-1C25F5C572FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {D5941EB2-3273-472D-9EA1-1C25F5C572FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {D5941EB2-3273-472D-9EA1-1C25F5C572FC}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {C49C74FD-E1C2-4310-9190-9996177902CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {C49C74FD-E1C2-4310-9190-9996177902CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {C49C74FD-E1C2-4310-9190-9996177902CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {C49C74FD-E1C2-4310-9190-9996177902CD}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {19317603-ECDA-4CB8-B781-619CF69F2979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {19317603-ECDA-4CB8-B781-619CF69F2979}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {19317603-ECDA-4CB8-B781-619CF69F2979}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {19317603-ECDA-4CB8-B781-619CF69F2979}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {7D335775-8D76-45B4-A035-695C88EB0134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {7D335775-8D76-45B4-A035-695C88EB0134}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {7D335775-8D76-45B4-A035-695C88EB0134}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {7D335775-8D76-45B4-A035-695C88EB0134}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {C8AC65D0-EE58-4CC1-880C-F97D78DFF83F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {C8AC65D0-EE58-4CC1-880C-F97D78DFF83F}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {C8AC65D0-EE58-4CC1-880C-F97D78DFF83F}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {C8AC65D0-EE58-4CC1-880C-F97D78DFF83F}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {7106E695-F1A1-499F-9E56-7866E5B3C75E} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSBuild Flame Graph 2 | 3 | Explore MSBuild executions to find why your C++ builds are slow. 4 | 5 | This is the flame graph for a default Visual Studio 2015 C++ project: 6 | 7 | ![Flame Graph: default Visual Studio 2015](./readme-samples/blank-project.png "Flame graph: default Visual Studio 2015 project") 8 | 9 | This graph represents a build from [this repository](https://github.com/randomascii/main/tree/master/xperf/vc_parallel_compiles). 10 | 11 | ![Flame Graph: Bruce Dawson's parallel projects](./readme-samples/random-ascii-parallel.png "Flame graph: Bruce Dawson's parallel projects") 12 | 13 | I wrote about the steps I took to build the initial version of the tool [in this post series](http://coding-scars.com/investigating-cpp-compile-times-0/). 14 | 15 | ![UI screenshot](./readme-samples/ui-screenshot.png "UI screenshot") 16 | 17 | ## Features 18 | 19 | * Uses MSBuild 15: builds VS2015 and VS2017 C++ projects. 20 | * Processes `/Bt+` and `/time+` MSVC flags: 21 | ![Flame Graph: /Bt+ and /time+](./readme-samples/bt-plus-time-plus.png "Flame graph: /Bt+ and /time+") 22 | * Processes `/d1reportTime` MSVC flag (exclusive to VS2017 projects): 23 | ![Flame Graph: /d1reportTime](./readme-samples/d1reportTime.png "Flame graph: /d1reportTime") 24 | * Builds projects into an `Events.json` file, converts it into `Trace.json` in a separate step. 25 | * Can theoretically build other MSBuild-based projects, but is only tested on C++ ones. 26 | 27 | ## Getting started 28 | 29 | * Install Visual Studio 2017+ Community Edition. 30 | * Install .NET Framework v4.6.2 SDK. 31 | * Clone repository. 32 | * Open solution, build and run. 33 | 34 | ## Codebase overview 35 | 36 | Should you want to explore what's in the repository, these are the main parts: 37 | 38 | ### Projects 39 | 40 | * `Builder`: contains the UI (built in WPF) to interact with the tool. 41 | * `MSBuildWrapper`: defines MSBuild loggers, interacts with MSBuild API and converts MSBuild events to custom abstractions. 42 | * `BuildTimeline`: represents timelines and everything it needs, from events to entries. 43 | * `TimelineSerializer`: includes a way to convert a `Timeline` to a Google Chrome's trace. New trace formats can be added. 44 | * `Model`: represents data to be used by other projects. 45 | 46 | ### Main flow 47 | 48 | * As part of `MSBuildWrapper/Compilation/Compilation.cs`, when a build starts every MSBuild event is displayed in the UI and gets stored in memory. 49 | * When the build finishes, most events get converted into a custom format (some events and properties are discarded). 50 | * When the build is finished, `Builder/ViewModel/Commands.cs` stores every custom event in an `Events.json` file. 51 | * A `Trace.json` file can be exported from an `Events.json` file via `Builder/ViewModel/Commands.cs`. This is useful to build different timelines (even to different formats) without having to repeat the build. 52 | 53 | ## License 54 | 55 | This project is released under [GNU GPLv3](https://github.com/MetanoKid/msbuild-flame-graph/blob/master/LICENSE.md) license. 56 | 57 | I started this project thanks to the information I gathered from the community, so I wanted to give something back. You are encouraged to alter it in any way you want, but please continue making it public so the community can benefit from it. 58 | 59 | ## Acknowledgements 60 | 61 | * Thanks to [@aras_p](https://twitter.com/aras_p) for his blog posts investigating build times, [this one](https://aras-p.info/blog/2019/01/16/time-trace-timeline-flame-chart-profiler-for-Clang/) was the main inspiration for this tool. 62 | * Thanks to Microsoft's dev team for building these API and new tools like [vcperf](https://github.com/microsoft/vcperf) or [MSBuild Structured Log Viewer](https://github.com/KirillOsenkov/MSBuildStructuredLog). 63 | -------------------------------------------------------------------------------- /readme-samples/blank-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetanoKid/msbuild-flame-graph/5fc0c04c22905ea9f158bb4731ab542b3c072aff/readme-samples/blank-project.png -------------------------------------------------------------------------------- /readme-samples/bt-plus-time-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetanoKid/msbuild-flame-graph/5fc0c04c22905ea9f158bb4731ab542b3c072aff/readme-samples/bt-plus-time-plus.png -------------------------------------------------------------------------------- /readme-samples/d1reportTime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetanoKid/msbuild-flame-graph/5fc0c04c22905ea9f158bb4731ab542b3c072aff/readme-samples/d1reportTime.png -------------------------------------------------------------------------------- /readme-samples/random-ascii-parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetanoKid/msbuild-flame-graph/5fc0c04c22905ea9f158bb4731ab542b3c072aff/readme-samples/random-ascii-parallel.png -------------------------------------------------------------------------------- /readme-samples/ui-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetanoKid/msbuild-flame-graph/5fc0c04c22905ea9f158bb4731ab542b3c072aff/readme-samples/ui-screenshot.png -------------------------------------------------------------------------------- /src/BuildTimeline/BuildData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Model; 3 | 4 | namespace BuildTimeline 5 | { 6 | public class BuildData 7 | { 8 | public BuildConfiguration BuildConfiguration { get; set; } 9 | public List Events { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BuildTimeline/BuildTimeline.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {19317603-ECDA-4CB8-B781-619CF69F2979} 8 | Library 9 | Properties 10 | BuildTimeline 11 | BuildTimeline 12 | v4.6.2 13 | 512 14 | $(SolutionDir)obj\$(Configuration)\$(MSBuildProjectName) 15 | $(SolutionDir)bin\$(Configuration)\$(MSBuildProjectName) 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {c49c74fd-e1c2-4310-9190-9996177902cd} 74 | Model 75 | 76 | 77 | 78 | 85 | -------------------------------------------------------------------------------- /src/BuildTimeline/Entries/BuildEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BuildTimeline 4 | { 5 | public class BuildEntry 6 | { 7 | // the event that started this entry 8 | public Event StartEvent { get; } 9 | 10 | // the event that ended this entry 11 | public Event EndEvent { get; private set; } 12 | 13 | // the context for both start and end events 14 | public EventContext Context 15 | { 16 | get 17 | { 18 | return StartEvent?.Context; 19 | } 20 | } 21 | 22 | // parent entry, if any 23 | public BuildEntry Parent { get; private set; } 24 | 25 | // child entries, if any 26 | public List ChildEntries { get; private set; } 27 | 28 | // child events (including those grouped within the child entries, but not the start/end from this entry), if any 29 | public List ChildEvents { get; private set; } 30 | 31 | public BuildEntry(Event startEvent) 32 | { 33 | ChildEntries = new List(); 34 | ChildEvents = new List(); 35 | 36 | StartEvent = startEvent; 37 | } 38 | 39 | public void CloseWith(Event endEvent) 40 | { 41 | EndEvent = endEvent; 42 | } 43 | 44 | public void AddChild(BuildEntry entry) 45 | { 46 | ChildEntries.Add(entry); 47 | entry.Parent = this; 48 | } 49 | 50 | public void AddChild(Event e) 51 | { 52 | ChildEvents.Add(e); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/BuildTimeline/Entries/TimelineBuildEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace BuildTimeline 6 | { 7 | public class TimelineBuildEntry : TimelineEntry 8 | { 9 | public BuildEntry BuildEntry { get; private set; } 10 | 11 | public TimelineBuildEntry(BuildEntry buildEntry, BuildData buildData) : 12 | base(ExtractNameFrom(buildEntry, buildData), 13 | buildEntry.Context == null ? 0 : buildEntry.Context.NodeId, 14 | buildEntry.StartEvent.Timestamp, 15 | buildEntry.EndEvent.Timestamp) 16 | { 17 | BuildEntry = buildEntry; 18 | } 19 | 20 | private static string ExtractNameFrom(BuildEntry buildEntry, BuildData buildData) 21 | { 22 | Event e = buildEntry.StartEvent; 23 | string name = null; 24 | 25 | if (e is BuildStartedEvent) 26 | { 27 | StringBuilder builder = new StringBuilder(); 28 | builder.Append($"{buildData.BuildConfiguration.Target}"); 29 | builder.Append($" - {buildData.BuildConfiguration.Configuration}|{buildData.BuildConfiguration.Platform}"); 30 | builder.Append($" - Max parallel: {buildData.BuildConfiguration.MaxParallelProjects} projects, {buildData.BuildConfiguration.MaxParallelCLTasksPerProject} CL tasks per project"); 31 | builder.Append($" - {buildData.BuildConfiguration.SolutionPath}"); 32 | 33 | name = builder.ToString(); 34 | } 35 | else if (e is ProjectStartedEvent) 36 | { 37 | name = (e as ProjectStartedEvent).ProjectFile; 38 | 39 | // find the longest common path between project and solution, then remove it from project file path 40 | string[] solutionPath = buildData.BuildConfiguration.SolutionPath.Split(Path.DirectorySeparatorChar); 41 | string[] projectPath = name.Split(Path.DirectorySeparatorChar); 42 | 43 | int firstDifferenceIndex = -1; 44 | int minPathSteps = Math.Min(solutionPath.Length, projectPath.Length); 45 | for(int i = 0; i < minPathSteps; ++i) 46 | { 47 | if(solutionPath[i] != projectPath[i]) 48 | { 49 | firstDifferenceIndex = i; 50 | break; 51 | } 52 | } 53 | 54 | if(firstDifferenceIndex >= 0) 55 | { 56 | name = String.Join(Path.DirectorySeparatorChar.ToString(), projectPath, firstDifferenceIndex, projectPath.Length - firstDifferenceIndex); 57 | } 58 | } 59 | else if (e is TargetStartedEvent) 60 | { 61 | name = (e as TargetStartedEvent).TargetName; 62 | } 63 | else if (e is TaskStartedEvent) 64 | { 65 | name = (e as TaskStartedEvent).TaskName; 66 | } 67 | 68 | return name; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/BuildTimeline/Entries/TimelineEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | 6 | namespace BuildTimeline 7 | { 8 | public class TimelineEntry 9 | { 10 | public DateTime StartTimestamp { get; private set; } 11 | public DateTime EndTimestamp { get; private set; } 12 | 13 | public TimeSpan ElapsedTime 14 | { 15 | get 16 | { 17 | return EndTimestamp - StartTimestamp; 18 | } 19 | } 20 | 21 | public Guid GUID { get; } 22 | public string Name { get; set; } 23 | public TimelineEntry Parent { get; private set; } 24 | public List ChildEntries { get; } 25 | public ThreadAffinity ThreadAffinity { get; } 26 | public int NodeId { get; } 27 | 28 | public TimelineEntry(string name, int nodeId, DateTime startTimestamp, DateTime endTimestamp) 29 | { 30 | GUID = Guid.NewGuid(); 31 | Name = name; 32 | Parent = null; 33 | ChildEntries = new List(); 34 | ThreadAffinity = new ThreadAffinity(); 35 | NodeId = nodeId; 36 | StartTimestamp = startTimestamp; 37 | EndTimestamp = endTimestamp; 38 | } 39 | 40 | public void AddChild(TimelineEntry entry) 41 | { 42 | ChildEntries.Add(entry); 43 | entry.Parent = this; 44 | } 45 | 46 | public bool OverlapsWith(TimelineEntry other) 47 | { 48 | return NodeId == other.NodeId && 49 | StartTimestamp < other.EndTimestamp && 50 | other.StartTimestamp < EndTimestamp; 51 | } 52 | 53 | public bool IsAncestorOf(TimelineEntry other) 54 | { 55 | if(other == null) 56 | { 57 | return false; 58 | } 59 | 60 | if(this == other.Parent) 61 | { 62 | return true; 63 | } 64 | 65 | return IsAncestorOf(other.Parent); 66 | } 67 | 68 | public void SetEndTimestamp(DateTime timestamp) 69 | { 70 | Debug.Assert(StartTimestamp <= timestamp); 71 | EndTimestamp = timestamp; 72 | } 73 | 74 | public void FitChildEntries() 75 | { 76 | if(ChildEntries.Count == 0) 77 | { 78 | return; 79 | } 80 | 81 | // we've defined fitting only when overflow occurs towards the end 82 | Debug.Assert(ChildEntries.First().StartTimestamp >= StartTimestamp); 83 | 84 | DateTime childrenLastEndTimestamp = ChildEntries.Last().EndTimestamp; 85 | DateTime maxEndTimestamp = childrenLastEndTimestamp > EndTimestamp ? childrenLastEndTimestamp : EndTimestamp; 86 | 87 | TimeSpan elapsedTimeWithOverflow = maxEndTimestamp - StartTimestamp; 88 | double scale = elapsedTimeWithOverflow.Ticks > 0 ? (double) ElapsedTime.Ticks / elapsedTimeWithOverflow.Ticks : 1.0; 89 | Debug.Assert(scale <= 1.0); 90 | 91 | if(scale < 1.0) 92 | { 93 | ScaleTimestamps(scale); 94 | } 95 | 96 | foreach(TimelineEntry child in ChildEntries) 97 | { 98 | child.FitChildEntries(); 99 | } 100 | } 101 | 102 | private void ScaleTimestamps(double scale) 103 | { 104 | foreach (TimelineEntry child in ChildEntries) 105 | { 106 | // take the local offset from parent's start (we can't modify it in absolute values, but relative ones) 107 | TimeSpan originalLocalStartOffset = child.StartTimestamp - StartTimestamp; 108 | TimeSpan originalLocalEndOffset = child.EndTimestamp - StartTimestamp; 109 | 110 | // scale that local offset 111 | TimeSpan scaledLocalStartOffset = TimeSpan.FromTicks((long) (originalLocalStartOffset.Ticks * scale)); 112 | TimeSpan scaledLocalEndOffset = TimeSpan.FromTicks((long) (originalLocalEndOffset.Ticks * scale)); 113 | 114 | // calculate new timestamps 115 | child.StartTimestamp = StartTimestamp + scaledLocalStartOffset; 116 | child.EndTimestamp = StartTimestamp + scaledLocalEndOffset; 117 | 118 | Debug.Assert(child.StartTimestamp >= StartTimestamp); 119 | Debug.Assert(child.EndTimestamp <= EndTimestamp); 120 | 121 | // because we've moved the origin (start timestamp) our relative offsets are no longer valid! 122 | // so, apply the same offset to all children (absolute value, because it's an offset and not a timestamp) 123 | child.MoveChildrenTimestamps(scaledLocalStartOffset - originalLocalStartOffset); 124 | 125 | // propagate it 126 | child.ScaleTimestamps(scale); 127 | } 128 | } 129 | 130 | private void MoveChildrenTimestamps(TimeSpan offset) 131 | { 132 | foreach(TimelineEntry child in ChildEntries) 133 | { 134 | child.StartTimestamp += offset; 135 | child.EndTimestamp += offset; 136 | 137 | child.MoveChildrenTimestamps(offset); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/BuildFinishedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class BuildFinishedEvent : Event 4 | { 5 | // whether the build finished successfully 6 | public bool Succeeded { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/BuildStartedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class BuildStartedEvent : Event 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/Context/EventContext.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class EventContext 4 | { 5 | // the ID of this context 6 | public int ContextId { get; set; } 7 | 8 | // the ID of the node where this context lives in 9 | public int NodeId { get; set; } 10 | 11 | public override bool Equals(object obj) 12 | { 13 | EventContext other = obj as EventContext; 14 | return other != null && 15 | ContextId == other.ContextId && 16 | NodeId == other.NodeId; 17 | } 18 | 19 | public override int GetHashCode() 20 | { 21 | return base.GetHashCode(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/Context/MessageEventContext.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class MessageEventContext : EventContext 4 | { 5 | // the ID of the project where this context lives in, if any 6 | public int? ProjectId { get; set; } 7 | 8 | // the ID of the target where this context lives in, if any 9 | public int? TargetId { get; set; } 10 | 11 | // the ID of the task where this context lives in, if any 12 | public int? TaskId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/Context/ProjectEventContext.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class ProjectEventContext : EventContext 4 | { 5 | // the ID of the project where this context lives in 6 | public int ProjectId { get; set; } 7 | 8 | public override bool Equals(object obj) 9 | { 10 | ProjectEventContext other = obj as ProjectEventContext; 11 | return other != null && 12 | base.Equals(obj) && 13 | ProjectId == other.ProjectId; 14 | } 15 | 16 | public override int GetHashCode() 17 | { 18 | return base.GetHashCode(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/Context/TargetEventContext.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class TargetEventContext : EventContext 4 | { 5 | // the ID of the project where this context lives in 6 | public int ProjectId { get; set; } 7 | 8 | // the ID of the target where this context lives in 9 | public int TargetId { get; set; } 10 | 11 | public override bool Equals(object obj) 12 | { 13 | TargetEventContext other = obj as TargetEventContext; 14 | return other != null && 15 | base.Equals(obj) && 16 | ProjectId == other.ProjectId && 17 | TargetId == other.TargetId; 18 | } 19 | 20 | public override int GetHashCode() 21 | { 22 | return base.GetHashCode(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/Context/TaskEventContext.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class TaskEventContext : EventContext 4 | { 5 | // the ID of the project where this context lives in 6 | public int ProjectId { get; set; } 7 | 8 | // the ID of the target where this context lives in 9 | public int TargetId { get; set; } 10 | 11 | // the ID of the task where this context lives in 12 | public int TaskId { get; set; } 13 | 14 | public override bool Equals(object obj) 15 | { 16 | TaskEventContext other = obj as TaskEventContext; 17 | return other != null && 18 | base.Equals(obj) && 19 | ProjectId == other.ProjectId && 20 | TargetId == other.TargetId && 21 | TaskId == other.TaskId; 22 | } 23 | 24 | public override int GetHashCode() 25 | { 26 | return base.GetHashCode(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/ErrorEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class ErrorEvent : MessageEvent 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/Event.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BuildTimeline 4 | { 5 | public class Event 6 | { 7 | // context this event was created in 8 | public EventContext Context { get; set; } 9 | 10 | // information from this event 11 | public string Message { get; set; } 12 | 13 | // instant in time where this event was created 14 | public DateTime Timestamp { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/MessageEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class MessageEvent : Event 4 | { 5 | // code associated with the event 6 | public string Code { get; set; } 7 | 8 | // path to the file that contains the project data 9 | public string ProjectFile { get; set; } 10 | 11 | // path to the file related to the message, if any 12 | public string File { get; set; } 13 | 14 | // the custom sub-type of the event (as reported by MSBuild) 15 | public string Subcategory { get; set; } 16 | 17 | // start line where the message points to, if any 18 | public int LineStart { get; set; } 19 | 20 | // end line where the message points to, if any 21 | public int LineEnd { get; set; } 22 | 23 | // start column where the message points to, if any 24 | public int ColumnStart { get; set; } 25 | 26 | // end column where the message points to, if any 27 | public int ColumnEnd { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/ProjectFinishedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class ProjectFinishedEvent : Event 4 | { 5 | // path to the file that contains the project data 6 | public string ProjectFile { get; set; } 7 | 8 | // whether the project built successfully 9 | public bool Succeeded { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/ProjectStartedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class ProjectStartedEvent : Event 4 | { 5 | // context of the parent project that started this project, if any 6 | public ProjectEventContext ParentEventContext { get; set; } 7 | 8 | // path to the file that contains the project data 9 | public string ProjectFile { get; set; } 10 | 11 | // the ID of the project, used within event contexts 12 | public int ProjectId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/TargetFinishedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class TargetFinishedEvent : Event 4 | { 5 | // path to the file that contains the project data 6 | public string ProjectFile { get; set; } 7 | 8 | // whether the target built successfully 9 | public bool Succeeded { get; set; } 10 | 11 | // path to the file that contains the target data 12 | public string TargetFile { get; set; } 13 | 14 | // name of the target started with this event 15 | public string TargetName { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/TargetStartedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class TargetStartedEvent : Event 4 | { 5 | // name of the target that caused this one to build, if any 6 | public string ParentTarget { get; set; } 7 | 8 | // path to the file that contains the project data 9 | public string ProjectFile { get; set; } 10 | 11 | // path to the file that contains the target data 12 | public string TargetFile { get; set; } 13 | 14 | // name of the target started with this event 15 | public string TargetName { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/TaskFinishedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class TaskFinishedEvent : Event 4 | { 5 | // path to the file that contains the project data 6 | public string ProjectFile { get; set; } 7 | 8 | // whether the task built successfully 9 | public bool Succeeded { get; set; } 10 | 11 | // path to the file that contains the task data 12 | public string TaskFile { get; set; } 13 | 14 | // name of the task started with this event 15 | public string TaskName { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/TaskStartedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class TaskStartedEvent : Event 4 | { 5 | // path to the file that contains the project data 6 | public string ProjectFile { get; set; } 7 | 8 | // path to the file that contains the task data 9 | public string TaskFile { get; set; } 10 | 11 | // name of the task started with this event 12 | public string TaskName { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/BuildTimeline/Events/WarningEvent.cs: -------------------------------------------------------------------------------- 1 | namespace BuildTimeline 2 | { 3 | public class WarningEvent : MessageEvent 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildTimeline/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("BuildTimeline")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("BuildTimeline")] 12 | [assembly: AssemblyCopyright("Copyright © 2020")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("19317603-ecda-4cb8-b781-619cf69f2979")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /src/BuildTimeline/Timeline/Builder/TimelineBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace BuildTimeline 6 | { 7 | // alias for the group of root timeline entries per 8 | using PerNodeThreadRootEntries = Dictionary, List>; 9 | 10 | class TimelineBuilderContext 11 | { 12 | public List OpenBuildEntries; 13 | public List OpenProjectEntries; 14 | public List OpenTargetEntries; 15 | public List OpenTaskEntries; 16 | 17 | public bool HasOpenBuilds 18 | { 19 | get 20 | { 21 | return OpenBuildEntries.Count > 0; 22 | } 23 | } 24 | 25 | public bool HasOpenProjects 26 | { 27 | get 28 | { 29 | return OpenProjectEntries.Count > 0; 30 | } 31 | } 32 | 33 | public bool HasOpenTargets 34 | { 35 | get 36 | { 37 | return OpenTargetEntries.Count > 0; 38 | } 39 | } 40 | 41 | public bool HasOpenTasks 42 | { 43 | get 44 | { 45 | return OpenTaskEntries.Count > 0; 46 | } 47 | } 48 | 49 | public BuildEntry RootEntry { get; set; } 50 | 51 | public TimelineBuilderContext() 52 | { 53 | OpenBuildEntries = new List(); 54 | OpenProjectEntries = new List(); 55 | OpenTargetEntries = new List(); 56 | OpenTaskEntries = new List(); 57 | } 58 | } 59 | 60 | public class TimelineBuilder 61 | { 62 | private BuildData m_buildData; 63 | 64 | public TimelineBuilder(BuildData buildData) 65 | { 66 | m_buildData = buildData; 67 | } 68 | 69 | public Timeline Build(TimelineEntryPostProcessor.Processor perEntryPostProcessors) 70 | { 71 | TimelineBuilderContext context = ProcessEvents(m_buildData.Events); 72 | Debug.Assert(!context.HasOpenBuilds); 73 | Debug.Assert(!context.HasOpenProjects); 74 | Debug.Assert(!context.HasOpenTargets); 75 | Debug.Assert(!context.HasOpenTasks); 76 | Debug.Assert(context.RootEntry != null); 77 | 78 | Timeline timeline = BuildTimelineFrom(m_buildData, context); 79 | if(perEntryPostProcessors != null) 80 | { 81 | PostProcess(timeline, perEntryPostProcessors); 82 | } 83 | 84 | CalculateParallelEntries(timeline); 85 | 86 | EnsureNoEntryOverflowsParent(timeline); 87 | 88 | return timeline; 89 | } 90 | 91 | private TimelineBuilderContext ProcessEvents(List events) 92 | { 93 | TimelineBuilderContext context = new TimelineBuilderContext(); 94 | 95 | foreach (Event e in events) 96 | { 97 | if (e is BuildStartedEvent) 98 | { 99 | ProcessBuildStartEvent(e as BuildStartedEvent, context); 100 | } 101 | else if (e is BuildFinishedEvent) 102 | { 103 | ProcessBuildEndEvent(e as BuildFinishedEvent, context); 104 | } 105 | else if (e is ProjectStartedEvent) 106 | { 107 | ProcessProjectStartEvent(e as ProjectStartedEvent, context); 108 | } 109 | else if (e is ProjectFinishedEvent) 110 | { 111 | ProcessProjectEndEvent(e as ProjectFinishedEvent, context); 112 | } 113 | else if (e is TargetStartedEvent) 114 | { 115 | ProcessTargetStartEvent(e as TargetStartedEvent, context); 116 | } 117 | else if (e is TargetFinishedEvent) 118 | { 119 | ProcessTargetEndEvent(e as TargetFinishedEvent, context); 120 | } 121 | else if (e is TaskStartedEvent) 122 | { 123 | ProcessTaskStartEvent(e as TaskStartedEvent, context); 124 | } 125 | else if (e is TaskFinishedEvent) 126 | { 127 | ProcessTaskEndEvent(e as TaskFinishedEvent, context); 128 | } 129 | else if (e is MessageEvent) 130 | { 131 | ProcessMessageEvent(e as MessageEvent, context); 132 | } 133 | else if (e is WarningEvent) 134 | { 135 | ProcessMessageEvent(e as WarningEvent, context); 136 | } 137 | else if (e is ErrorEvent) 138 | { 139 | ProcessMessageEvent(e as ErrorEvent, context); 140 | } 141 | } 142 | 143 | return context; 144 | } 145 | 146 | private void ProcessBuildStartEvent(BuildStartedEvent e, TimelineBuilderContext context) 147 | { 148 | Debug.Assert(!context.HasOpenBuilds); 149 | Debug.Assert(!context.HasOpenProjects); 150 | Debug.Assert(!context.HasOpenTargets); 151 | Debug.Assert(!context.HasOpenTasks); 152 | Debug.Assert(context.RootEntry == null); 153 | 154 | BuildEntry buildEntry = new BuildEntry(e); 155 | context.OpenBuildEntries.Add(buildEntry); 156 | context.RootEntry = buildEntry; 157 | } 158 | 159 | private void ProcessBuildEndEvent(BuildFinishedEvent e, TimelineBuilderContext context) 160 | { 161 | Debug.Assert(context.OpenBuildEntries.Count == 1); 162 | Debug.Assert(!context.HasOpenProjects); 163 | Debug.Assert(!context.HasOpenTargets); 164 | Debug.Assert(!context.HasOpenTasks); 165 | 166 | BuildEntry buildEntry = context.OpenBuildEntries[0]; 167 | Debug.Assert(buildEntry != null); 168 | 169 | buildEntry.CloseWith(e); 170 | context.OpenBuildEntries.Remove(buildEntry); 171 | } 172 | 173 | private void ProcessProjectStartEvent(ProjectStartedEvent e, TimelineBuilderContext context) 174 | { 175 | Debug.Assert(context.HasOpenBuilds); 176 | 177 | BuildEntry projectEntry = new BuildEntry(e); 178 | context.OpenProjectEntries.Add(projectEntry); 179 | 180 | // projects always have a parent, we know which via parent event context 181 | BuildEntry parentEntry = null; 182 | 183 | // no parent event context mean we've been spawned from the build itself 184 | if(e.ParentEventContext == null) 185 | { 186 | parentEntry = context.RootEntry; 187 | } 188 | else 189 | { 190 | // a project's parent is a task, although the parent context refers to the task project's 191 | parentEntry = context.OpenTaskEntries.Find(taskEntry => 192 | { 193 | TaskEventContext taskContext = taskEntry.Context as TaskEventContext; 194 | Debug.Assert(taskContext != null); 195 | 196 | return taskContext.ContextId == e.ParentEventContext.ContextId && 197 | taskContext.ProjectId == e.ParentEventContext.ProjectId; 198 | }); 199 | 200 | // finding no task with matching data means our build was requested by a task that's already completed 201 | // we may have some luck finding the project that spawned that task 202 | if(parentEntry == null) 203 | { 204 | // our build couldn't be started before within the same node, hence it was scheduled 205 | Debug.Assert(e.Context.NodeId != e.ParentEventContext.NodeId); 206 | 207 | parentEntry = context.OpenProjectEntries.Find(otherProjectEntry => 208 | { 209 | ProjectEventContext projectContext = otherProjectEntry.Context as ProjectEventContext; 210 | Debug.Assert(projectContext != null); 211 | 212 | return projectContext.ContextId == e.ParentEventContext.ContextId && 213 | projectContext.ProjectId == e.ParentEventContext.ProjectId; 214 | }); 215 | 216 | // the project may have also finished 217 | if(parentEntry == null) 218 | { 219 | // just consider the build itself our parent 220 | parentEntry = context.RootEntry; 221 | } 222 | } 223 | } 224 | 225 | Debug.Assert(parentEntry != null); 226 | parentEntry.AddChild(e); 227 | parentEntry.AddChild(projectEntry); 228 | } 229 | 230 | private void ProcessProjectEndEvent(ProjectFinishedEvent e, TimelineBuilderContext context) 231 | { 232 | Debug.Assert(context.HasOpenBuilds); 233 | 234 | BuildEntry projectEntry = context.OpenProjectEntries.Find(_ => _.Context.Equals(e.Context)); 235 | Debug.Assert(projectEntry != null); 236 | 237 | projectEntry.Parent.AddChild(e); 238 | projectEntry.CloseWith(e); 239 | context.OpenProjectEntries.Remove(projectEntry); 240 | } 241 | 242 | private void ProcessTargetStartEvent(TargetStartedEvent e, TimelineBuilderContext context) 243 | { 244 | Debug.Assert(context.HasOpenBuilds); 245 | Debug.Assert(context.HasOpenProjects); 246 | 247 | BuildEntry targetEntry = new BuildEntry(e); 248 | context.OpenTargetEntries.Add(targetEntry); 249 | 250 | // a target's parent is a Project 251 | TargetEventContext targetContext = targetEntry.Context as TargetEventContext; 252 | Debug.Assert(targetContext != null); 253 | BuildEntry parentEntry = context.OpenProjectEntries.Find(projectEntry => 254 | { 255 | ProjectEventContext projectContext = projectEntry.Context as ProjectEventContext; 256 | Debug.Assert(projectContext != null); 257 | 258 | return projectContext.ContextId == targetContext.ContextId && 259 | projectContext.ProjectId == targetContext.ProjectId; 260 | }); 261 | 262 | Debug.Assert(parentEntry != null); 263 | parentEntry.AddChild(e); 264 | parentEntry.AddChild(targetEntry); 265 | } 266 | 267 | private void ProcessTargetEndEvent(TargetFinishedEvent e, TimelineBuilderContext context) 268 | { 269 | Debug.Assert(context.HasOpenBuilds); 270 | Debug.Assert(context.HasOpenProjects); 271 | 272 | BuildEntry targetEntry = context.OpenTargetEntries.Find(_ => _.Context.Equals(e.Context)); 273 | Debug.Assert(targetEntry != null); 274 | 275 | targetEntry.Parent.AddChild(e); 276 | targetEntry.CloseWith(e); 277 | context.OpenTargetEntries.Remove(targetEntry); 278 | } 279 | 280 | private void ProcessTaskStartEvent(TaskStartedEvent e, TimelineBuilderContext context) 281 | { 282 | Debug.Assert(context.HasOpenBuilds); 283 | Debug.Assert(context.HasOpenProjects); 284 | Debug.Assert(context.HasOpenTargets); 285 | 286 | BuildEntry taskEntry = new BuildEntry(e); 287 | context.OpenTaskEntries.Add(taskEntry); 288 | 289 | // a task's parent is a Target 290 | TaskEventContext taskContext = taskEntry.Context as TaskEventContext; 291 | Debug.Assert(taskContext != null); 292 | BuildEntry parentEntry = context.OpenTargetEntries.Find(targetEntry => 293 | { 294 | TargetEventContext targetContext = targetEntry.Context as TargetEventContext; 295 | Debug.Assert(targetContext != null); 296 | 297 | return targetContext.ContextId == taskContext.ContextId && 298 | targetContext.ProjectId == taskContext.ProjectId && 299 | targetContext.TargetId == taskContext.TargetId; 300 | }); 301 | 302 | Debug.Assert(parentEntry != null); 303 | parentEntry.AddChild(e); 304 | parentEntry.AddChild(taskEntry); 305 | } 306 | 307 | private void ProcessTaskEndEvent(TaskFinishedEvent e, TimelineBuilderContext context) 308 | { 309 | Debug.Assert(context.HasOpenBuilds); 310 | Debug.Assert(context.HasOpenProjects); 311 | Debug.Assert(context.HasOpenTargets); 312 | 313 | BuildEntry taskEntry = context.OpenTaskEntries.Find(_ => _.Context.Equals(e.Context)); 314 | Debug.Assert(taskEntry != null); 315 | 316 | taskEntry.Parent.AddChild(e); 317 | taskEntry.CloseWith(e); 318 | context.OpenTaskEntries.Remove(taskEntry); 319 | } 320 | 321 | private void ProcessMessageEvent(MessageEvent e, TimelineBuilderContext context) 322 | { 323 | Debug.Assert(context.HasOpenBuilds); 324 | 325 | // a message can be executed as part of any entry: build, project, target or task 326 | BuildEntry parentEntry = null; 327 | 328 | // part of the build? 329 | if(e.Context == null) 330 | { 331 | parentEntry = context.RootEntry; 332 | } 333 | else 334 | { 335 | MessageEventContext messageContext = e.Context as MessageEventContext; 336 | 337 | // part of a task? 338 | if(messageContext.TaskId != null) 339 | { 340 | Debug.Assert(messageContext.ProjectId != null); 341 | Debug.Assert(messageContext.TargetId != null); 342 | 343 | parentEntry = context.OpenTaskEntries.Find(taskEntry => 344 | { 345 | TaskEventContext taskContext = taskEntry.Context as TaskEventContext; 346 | Debug.Assert(taskContext != null); 347 | 348 | return taskContext.NodeId == messageContext.NodeId && 349 | taskContext.ContextId == messageContext.ContextId && 350 | taskContext.ProjectId == messageContext.ProjectId && 351 | taskContext.TargetId == messageContext.TargetId && 352 | taskContext.TaskId == messageContext.TaskId; 353 | }); 354 | } 355 | // part of a target? 356 | else if(messageContext.TargetId != null) 357 | { 358 | Debug.Assert(messageContext.ProjectId != null); 359 | Debug.Assert(messageContext.TaskId == null); 360 | 361 | parentEntry = context.OpenTargetEntries.Find(targetEntry => 362 | { 363 | TargetEventContext targetContext = targetEntry.Context as TargetEventContext; 364 | Debug.Assert(targetContext != null); 365 | 366 | return targetContext.NodeId == messageContext.NodeId && 367 | targetContext.ContextId == messageContext.ContextId && 368 | targetContext.ProjectId == messageContext.ProjectId && 369 | targetContext.TargetId == messageContext.TargetId; 370 | }); 371 | } 372 | // part of a project? 373 | else if(messageContext.ProjectId != null) 374 | { 375 | Debug.Assert(messageContext.TargetId == null); 376 | Debug.Assert(messageContext.TaskId == null); 377 | 378 | parentEntry = context.OpenProjectEntries.Find(projectEntry => 379 | { 380 | ProjectEventContext projectContext = projectEntry.Context as ProjectEventContext; 381 | Debug.Assert(projectContext != null); 382 | 383 | return projectContext.NodeId == messageContext.NodeId && 384 | projectContext.ContextId == messageContext.ContextId && 385 | projectContext.ProjectId == messageContext.ProjectId; 386 | }); 387 | } 388 | // part of the build itself? 389 | else 390 | { 391 | parentEntry = context.RootEntry; 392 | } 393 | } 394 | 395 | Debug.Assert(parentEntry != null); 396 | parentEntry.AddChild(e); 397 | } 398 | 399 | private Timeline BuildTimelineFrom(BuildData buildData, TimelineBuilderContext context) 400 | { 401 | Timeline timeline = new Timeline(buildData.BuildConfiguration.MaxParallelProjects); 402 | 403 | // build belongs to NodeId 0, as reported by MSBuild, while other entries start at NodeId 1 404 | Debug.Assert(context.RootEntry.Context == null); 405 | TimelineBuildEntry topLevelTimelineBuildEntry = new TimelineBuildEntry(context.RootEntry, buildData); 406 | timeline.AddRoot(topLevelTimelineBuildEntry); 407 | 408 | // process other entries 409 | BuildTimelineEntries(timeline, topLevelTimelineBuildEntry, buildData); 410 | 411 | return timeline; 412 | } 413 | 414 | private void BuildTimelineEntries(Timeline timeline, TimelineBuildEntry parent, BuildData buildData) 415 | { 416 | foreach(BuildEntry childEntry in parent.BuildEntry.ChildEntries) 417 | { 418 | TimelineBuildEntry timelineEntry = new TimelineBuildEntry(childEntry, buildData); 419 | 420 | // same NodeId? there's a TimelineEntry hierarchy 421 | if(parent.BuildEntry.Context?.NodeId == childEntry.Context.NodeId) 422 | { 423 | parent.AddChild(timelineEntry); 424 | } 425 | // different NodeId? we've got a new root 426 | else 427 | { 428 | timeline.AddRoot(timelineEntry); 429 | } 430 | 431 | Debug.Assert(timelineEntry != null); 432 | BuildTimelineEntries(timeline, timelineEntry, buildData); 433 | } 434 | } 435 | 436 | private void CalculateParallelEntries(Timeline timeline) 437 | { 438 | PerNodeThreadRootEntries calculatedPerNodeThreadRootEntries = new PerNodeThreadRootEntries(); 439 | 440 | foreach(List rootsInNode in timeline.PerNodeRootEntries) 441 | { 442 | foreach(TimelineEntry root in rootsInNode) 443 | { 444 | CalculateParallelEntriesFor(root, calculatedPerNodeThreadRootEntries); 445 | } 446 | } 447 | } 448 | 449 | private void CalculateParallelEntriesFor(TimelineEntry entry, PerNodeThreadRootEntries nodeThreadRootEntries) 450 | { 451 | if(entry is TimelineBuildEntry) 452 | { 453 | entry.ThreadAffinity.SetParameters(entry.ThreadAffinity.ThreadId, 0, ThreadAffinity.s_OffsetMSBuildEntries); 454 | } 455 | 456 | if(entry.Parent != null) 457 | { 458 | // retrieve all of the overlapping siblings 459 | List overlappingSiblings = new List(); 460 | 461 | foreach(TimelineEntry sibling in entry.Parent.ChildEntries) 462 | { 463 | if(entry != sibling && entry.OverlapsWith(sibling)) 464 | { 465 | overlappingSiblings.Add(sibling); 466 | } 467 | } 468 | 469 | // we may have calculated some of the siblings, take their information into account 470 | foreach(TimelineEntry overlappingSibling in overlappingSiblings) 471 | { 472 | if(overlappingSibling.ThreadAffinity.Calculated) 473 | { 474 | entry.ThreadAffinity.AddInvalid(overlappingSibling.ThreadAffinity.ThreadId); 475 | } 476 | } 477 | } 478 | 479 | // also, check the overlapping root entries from each calculated thread within the same node 480 | foreach(var pair in nodeThreadRootEntries) 481 | { 482 | if(pair.Key.Item1 == entry.NodeId) 483 | { 484 | foreach(TimelineEntry root in pair.Value) 485 | { 486 | Debug.Assert(root.ThreadAffinity.Calculated); 487 | if(!root.IsAncestorOf(entry) && entry.OverlapsWith(root)) 488 | { 489 | entry.ThreadAffinity.AddInvalid(root.ThreadAffinity.ThreadId); 490 | } 491 | } 492 | } 493 | } 494 | 495 | // now calculate where we think the entry was executed 496 | entry.ThreadAffinity.Calculate(); 497 | 498 | // are we a new root? 499 | if(entry.Parent == null || entry.ThreadAffinity.ThreadId != entry.Parent.ThreadAffinity.ThreadId) 500 | { 501 | // get or create root list for the 502 | Tuple key = new Tuple(entry.NodeId, entry.ThreadAffinity.ThreadId); 503 | List rootsInNodeThread = null; 504 | if(!nodeThreadRootEntries.TryGetValue(key, out rootsInNodeThread)) 505 | { 506 | rootsInNodeThread = new List(); 507 | nodeThreadRootEntries[key] = rootsInNodeThread; 508 | } 509 | 510 | rootsInNodeThread.Add(entry); 511 | } 512 | 513 | // now that we've decided where the entry was executed, transfer this data to child entries 514 | foreach(TimelineEntry child in entry.ChildEntries) 515 | { 516 | child.ThreadAffinity.InheritDataFrom(entry.ThreadAffinity); 517 | } 518 | 519 | // continue with child entries 520 | foreach(TimelineEntry child in entry.ChildEntries) 521 | { 522 | CalculateParallelEntriesFor(child, nodeThreadRootEntries); 523 | } 524 | } 525 | 526 | private void PostProcess(Timeline timeline, TimelineEntryPostProcessor.Processor perEntryPostProcessors) 527 | { 528 | foreach(List rootsInNode in timeline.PerNodeRootEntries) 529 | { 530 | foreach(TimelineEntry root in rootsInNode) 531 | { 532 | PostProcess(root, perEntryPostProcessors); 533 | } 534 | } 535 | } 536 | 537 | private void PostProcess(TimelineEntry entry, TimelineEntryPostProcessor.Processor perEntryPostProcessors) 538 | { 539 | perEntryPostProcessors(entry); 540 | 541 | foreach(TimelineEntry child in entry.ChildEntries) 542 | { 543 | PostProcess(child, perEntryPostProcessors); 544 | } 545 | } 546 | 547 | private void EnsureNoEntryOverflowsParent(Timeline timeline) 548 | { 549 | foreach (List rootsInNode in timeline.PerNodeRootEntries) 550 | { 551 | foreach (TimelineEntry root in rootsInNode) 552 | { 553 | EnsureNoEntryOverflowsParent(root); 554 | } 555 | } 556 | } 557 | 558 | private void EnsureNoEntryOverflowsParent(TimelineEntry entry) 559 | { 560 | entry.FitChildEntries(); 561 | /* 562 | if (entry.ChildEntries.Count > 0) 563 | { 564 | DateTime childrenFirstStartTimestamp = entry.ChildEntries.First().StartTimestamp; 565 | DateTime childrenLastEndTimestamp = entry.ChildEntries.Last().EndTimestamp; 566 | 567 | // no child should start before its parent! overflow occurs on end timestamps 568 | Debug.Assert(childrenFirstStartTimestamp >= entry.StartTimestamp); 569 | 570 | if(childrenLastEndTimestamp > entry.EndTimestamp) 571 | { 572 | DateTime minDate = entry.StartTimestamp; 573 | DateTime maxDate = childrenLastEndTimestamp; 574 | TimeSpan elapsedWithOverflow = maxDate - minDate; 575 | TimeSpan elapsedParent = entry.ElapsedTime; 576 | 577 | double ratio = (double) elapsedParent.Ticks / elapsedWithOverflow.Ticks; 578 | entry.ScaleChildrenTimestamps(ratio); 579 | } 580 | else 581 | { 582 | foreach(TimelineEntry child in entry.ChildEntries) 583 | { 584 | EnsureNoEntryOverflowsParent(child); 585 | } 586 | } 587 | } 588 | */ 589 | } 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /src/BuildTimeline/Timeline/ThreadAffinity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace BuildTimeline 5 | { 6 | public class ThreadAffinity 7 | { 8 | public static readonly int s_OffsetMSBuildEntries = 1000; 9 | public static readonly int s_OffsetFromParentPostProcessedEntries = 100; 10 | public static readonly int s_OffsetFromParentPostProcessedEntriesIncrement = 1; 11 | 12 | private static readonly int s_DefaultBaseThreadId = 0; 13 | private static readonly int s_DefaultBaseOffset = 0; 14 | private static readonly int s_DefaultThreadIdCalculationIncrement = 1; 15 | 16 | public int ThreadId { get; private set; } 17 | public bool Calculated { get; private set; } 18 | 19 | private HashSet m_invalidThreadIDs; 20 | private int m_baseThreadId; 21 | private int m_baseOffset; 22 | private int m_threadIdCalculationIncrement; 23 | 24 | public ThreadAffinity() 25 | { 26 | m_baseThreadId = s_DefaultBaseThreadId; 27 | m_baseOffset = s_DefaultBaseOffset; 28 | m_threadIdCalculationIncrement = s_DefaultThreadIdCalculationIncrement; 29 | 30 | ThreadId = m_baseThreadId; 31 | Calculated = false; 32 | m_invalidThreadIDs = new HashSet(); 33 | } 34 | 35 | public void SetParameters(int baseThreadId, int baseOffset, int threadIdCalculationIncrement) 36 | { 37 | Debug.Assert(!Calculated); 38 | 39 | m_baseThreadId = baseThreadId; 40 | m_baseOffset = baseOffset; 41 | m_threadIdCalculationIncrement = threadIdCalculationIncrement; 42 | 43 | ThreadId = GetInitialThreadId(); 44 | } 45 | 46 | public void AddInvalid(int threadID) 47 | { 48 | m_invalidThreadIDs.Add(threadID); 49 | } 50 | 51 | public void Calculate() 52 | { 53 | Debug.Assert(!Calculated); 54 | 55 | // we've been set an ID but it's become invalid? 56 | int initialThreadId = GetInitialThreadId(); 57 | if(m_invalidThreadIDs.Contains(ThreadId)) 58 | { 59 | ThreadId = initialThreadId; 60 | } 61 | 62 | // find the first valid one 63 | for(int i = 0; m_invalidThreadIDs.Contains(ThreadId); ++i) 64 | { 65 | ThreadId = initialThreadId + i * m_threadIdCalculationIncrement; 66 | } 67 | 68 | Calculated = true; 69 | } 70 | 71 | public void InheritDataFrom(ThreadAffinity other) 72 | { 73 | Debug.Assert(!Calculated); 74 | Debug.Assert(other.Calculated); 75 | 76 | foreach(int invalidThreadId in other.m_invalidThreadIDs) 77 | { 78 | m_invalidThreadIDs.Add(invalidThreadId); 79 | } 80 | 81 | m_baseThreadId = other.ThreadId; 82 | 83 | ThreadId = GetInitialThreadId(); 84 | } 85 | 86 | private int GetInitialThreadId() 87 | { 88 | return m_baseThreadId + m_baseOffset; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/BuildTimeline/Timeline/Timeline.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BuildTimeline 4 | { 5 | public class Timeline 6 | { 7 | public List[] PerNodeRootEntries { get; private set; } 8 | 9 | public Timeline(int nodeCount) 10 | { 11 | // index 0 will be used for the build only 12 | PerNodeRootEntries = new List[nodeCount + 1]; 13 | 14 | for(int i = 0; i < PerNodeRootEntries.Length; ++i) 15 | { 16 | PerNodeRootEntries[i] = new List(); 17 | } 18 | } 19 | 20 | public void AddRoot(TimelineEntry root) 21 | { 22 | PerNodeRootEntries[root.NodeId].Add(root); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Builder/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Builder/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Builder/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace Builder 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Builder/Builder.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D5941EB2-3273-472D-9EA1-1C25F5C572FC} 8 | WinExe 9 | Properties 10 | Builder 11 | Builder 12 | v4.6.2 13 | 512 14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 4 16 | true 17 | $(SolutionDir)obj\$(Configuration)\$(MSBuildProjectName) 18 | $(SolutionDir)bin\$(Configuration)\$(MSBuildProjectName) 19 | 20 | 21 | 22 | AnyCPU 23 | true 24 | full 25 | false 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | 30 | 31 | AnyCPU 32 | pdbonly 33 | true 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 4.0 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | MSBuild:Compile 57 | Designer 58 | 59 | 60 | 61 | 62 | 63 | MSBuild:Compile 64 | Designer 65 | 66 | 67 | App.xaml 68 | Code 69 | 70 | 71 | MainWindow.xaml 72 | Code 73 | 74 | 75 | 76 | 77 | Code 78 | 79 | 80 | True 81 | True 82 | Resources.resx 83 | 84 | 85 | True 86 | Settings.settings 87 | True 88 | 89 | 90 | ResXFileCodeGenerator 91 | Resources.Designer.cs 92 | 93 | 94 | SettingsSingleFileGenerator 95 | Settings.Designer.cs 96 | 97 | 98 | 99 | Always 100 | 101 | 102 | Always 103 | 104 | 105 | Always 106 | 107 | 108 | Always 109 | 110 | 111 | Always 112 | 113 | 114 | 115 | 116 | Designer 117 | 118 | 119 | 120 | 121 | {19317603-ecda-4cb8-b781-619cf69f2979} 122 | BuildTimeline 123 | 124 | 125 | {c49c74fd-e1c2-4310-9190-9996177902cd} 126 | Model 127 | 128 | 129 | {c8ac65d0-ee58-4cc1-880c-f97d78dff83f} 130 | MSBuildWrapper 131 | 132 | 133 | {7d335775-8d76-45b4-a035-695c88eb0134} 134 | TimelineSerializer 135 | 136 | 137 | 138 | 139 | 13.0.1 140 | 141 | 142 | 143 | 150 | -------------------------------------------------------------------------------- /src/Builder/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 15 | 17 | 18 | 19 | 22 | 23 |