Почему msbuild работает так медленно, если с момента последней сборки решения ничего не изменилось?

У меня есть решение со 109 проектами. Сочетание всего: .NET, Silverlight, MVC, веб-приложений, консольных приложений.

Теперь я собираю его на консоли с помощью msbuild. На это нужно время. Понятно - много проектов, много файлов.

Но когда я строю его второй раз сразу после первого - это все равно занимает много времени, даже если на самом деле ничего не построено - журнал diag msbuild подтверждает это.

Например, вот сводка производительности задачи для первой полной сборки:

Task Performance Summary:
    4 ms  GetSilverlightItemsFromProperty            1 calls
   13 ms  Move                                       1 calls
   20 ms  GetReferenceAssemblyPaths                 27 calls
   28 ms  GetFrameworkPath                         190 calls
   29 ms  ValidateSilverlightFrameworkPaths        163 calls
   72 ms  AssignCulture                            192 calls
   75 ms  ResolveKeySource                         179 calls
   79 ms  MakeDir                                  200 calls
   95 ms  CreateProperty                           260 calls
  100 ms  CreateCSharpManifestResourceName         122 calls
  102 ms  Delete                                   442 calls
  112 ms  GenerateResource                           3 calls
  123 ms  CopyFilesToFolders                         1 calls
  177 ms  ReadLinesFromFile                        190 calls
  179 ms  CreateHtmlTestPage                        31 calls
  181 ms  CallTarget                               190 calls
  184 ms  GetSilverlightFrameworkPath              163 calls
  211 ms  Message                                  573 calls
  319 ms  CreateSilverlightAppManifest              97 calls
  354 ms  FileClassifier                           119 calls
  745 ms  ConvertToAbsolutePath                    190 calls
  868 ms  PackagePlatformExtensions                 94 calls
  932 ms  AssignProjectConfiguration               190 calls
 1625 ms  CategorizeSilverlightReferences          163 calls
 1722 ms  ResourcesGenerator                        60 calls
 2467 ms  WriteLinesToFile                         118 calls
 5589 ms  RemoveDuplicates                         380 calls
 8207 ms  FindUnderPath                            950 calls
17720 ms  XapPackager                               97 calls
38162 ms  Copy                                     857 calls
38934 ms  CompileXaml                              119 calls
40567 ms  Exec                                      14 calls
55275 ms  ValidateXaml                             119 calls
65845 ms  AssignTargetPath                         1140 calls
83792 ms  Csc                                      108 calls
105906 ms  ResolveAssemblyReference                 190 calls
1163988 ms  MSBuild                                  471 calls

претензии msbuild Time Elapsed 00:08:39.44

Отлично. Теперь я снова запускаю ту же командную строку и получаю следующее:

Task Performance Summary:
    1 ms  GetSilverlightItemsFromProperty            1 calls
   11 ms  WriteLinesToFile                           1 calls
   17 ms  GetReferenceAssemblyPaths                 27 calls
   24 ms  GetFrameworkPath                         190 calls
   32 ms  ValidateSilverlightFrameworkPaths        163 calls
   43 ms  CopyFilesToFolders                         1 calls
   47 ms  GenerateResource                           3 calls
   60 ms  ResolveKeySource                         179 calls
   66 ms  MakeDir                                  200 calls
   69 ms  AssignCulture                            192 calls
   70 ms  PackagePlatformExtensions                 94 calls
   76 ms  Delete                                   432 calls
   89 ms  CreateProperty                           260 calls
   98 ms  CreateCSharpManifestResourceName         122 calls
  136 ms  GetSilverlightFrameworkPath              163 calls
  156 ms  CallTarget                               190 calls
  182 ms  CreateHtmlTestPage                        31 calls
  207 ms  XapPackager                               97 calls
  215 ms  ReadLinesFromFile                        190 calls
  217 ms  Message                                  573 calls
  271 ms  CreateSilverlightAppManifest              97 calls
  350 ms  FileClassifier                           119 calls
  526 ms  ConvertToAbsolutePath                    190 calls
  795 ms  AssignProjectConfiguration               190 calls
 1658 ms  CategorizeSilverlightReferences          163 calls
 2237 ms  Exec                                       2 calls
 5703 ms  RemoveDuplicates                         380 calls
 6948 ms  Copy                                     426 calls
 7550 ms  FindUnderPath                            950 calls
17126 ms  CompileXaml                              119 calls
54495 ms  ValidateXaml                             119 calls
78953 ms  AssignTargetPath                         1140 calls
97374 ms  ResolveAssemblyReference                 190 calls
603295 ms  MSBuild                                  471 calls

msbuild утверждает Time Elapsed 00:05:25.70.

Это ставит следующие вопросы:

  1. Почему ResolveAssemblyReference занимает так много времени во второй сборке? Все файлы кеша, созданные в первой сборке, остались там. Ничего не изменилось. Так почему же это происходит почти так же, как и раньше - 97 секунд против 106 секунд?
  2. Почему ValidateXaml и CompileXaml вообще работают? Я имею в виду, что с момента полной сборки ничего не изменилось!

Теперь я повторяю тот же эксперимент, но на этот раз я использую devenv в командной строке вместо msbuild. Как и в случае с msbuild, параллельные сборки не используются, а уровень журнала находится на diag.

devenv не представляет такой красивой сводки в конце, ее нужно вручную собирать из сводок по проектам.

Результаты меня поразили. Я использовал следующий сценарий PowerShell для агрегирования прошедшего времени:

Param(
  [Parameter(Mandatory=$True,Position=1)][string]$log
)
[timespan]::FromMilliseconds((sls -SimpleMatch "Time Elapsed" $log |% {[timespan]::Parse(($_ -split ' ')[2]) } | measure -Sum TotalMilliseconds).Sum).ToString()

Для построения точно такого же решения из той же точки стояния с devenv в командной строке потребовалось 00:06:10.9000000 первая сборка и 00:00:03.1000000 вторая. Это всего 3 секунды !!!

Я также написал сценарий PowerShell для сбора статистики:

Param(
  [Parameter(Mandatory=$True,Position=1)][string]$log
)
$summary=@{}
cat $log |% { 
  if ($collect) {
    if ($_ -eq "") {
      $collect = $false;
    } else { 
      $tmp = ($_ -replace '\s+', ' ') -split ' ';
      $cur = $summary[$tmp[3]];
      if (!$cur) { 
        $cur = @(0, 0);
        $summary[$tmp[3]] = $cur;
      }
      $cur[0] += $tmp[1];
      $cur[1] += $tmp[4];
    }
  } else { 
    $collect = $_ -eq "Task Performance Summary:" 
  } 
}
$summary.Keys |% {
  $stats = $summary[$_];
  $ms = $stats[0];
  $calls = $stats[1];
  [string]::Format("{0,10} ms {1,-40} {2} calls", $ms,$_,$calls);
} | sort

Запуск его в журнале первой (полной) сборки дает следующий результат:

     5 ms ValidateSilverlightFrameworkPaths        82 calls
     7 ms Move                                     1 calls
     9 ms GetFrameworkPath                         108 calls
    11 ms GetReferenceAssemblyPaths                26 calls
    14 ms AssignCulture                            109 calls
    16 ms ReadLinesFromFile                        108 calls
    18 ms CreateCSharpManifestResourceName         61 calls
    18 ms ResolveKeySource                         97 calls
    23 ms Delete                                   268 calls
    26 ms CreateProperty                           131 calls
    41 ms MakeDir                                  118 calls
    66 ms CallTarget                               108 calls
    70 ms Message                                  326 calls
    75 ms ResolveNonMSBuildProjectOutput           104 calls
   101 ms GenerateResource                         1 calls
   107 ms GetSilverlightFrameworkPath              82 calls
   118 ms CreateHtmlTestPage                       16 calls
   153 ms FileClassifier                           60 calls
   170 ms CreateSilverlightAppManifest             49 calls
   175 ms AssignProjectConfiguration               108 calls
   279 ms ConvertToAbsolutePath                    108 calls
   891 ms CategorizeSilverlightReferences          82 calls
   926 ms PackagePlatformExtensions                47 calls
  1291 ms ResourcesGenerator                       60 calls
  2193 ms WriteLinesToFile                         108 calls
  3687 ms RemoveDuplicates                         216 calls
  5538 ms FindUnderPath                            540 calls
  6157 ms MSBuild                                  294 calls
 16496 ms Exec                                     4 calls
 19699 ms XapPackager                              49 calls
 21281 ms Copy                                     378 calls
 28362 ms ValidateXaml                             60 calls
 29526 ms CompileXaml                              60 calls
 66846 ms AssignTargetPath                         654 calls
 81650 ms Csc                                      108 calls
 82759 ms ResolveAssemblyReference                 108 calls

Теперь для второй сборки результаты следующие:

     1 ms AssignCulture                            1 calls
     1 ms CreateProperty                           1 calls
     1 ms Delete                                   2 calls
     1 ms ValidateSilverlightFrameworkPaths        1 calls
     3 ms AssignTargetPath                         6 calls
     3 ms ConvertToAbsolutePath                    1 calls
     3 ms PackagePlatformExtensions                1 calls
     3 ms ReadLinesFromFile                        1 calls
     3 ms ResolveKeySource                         1 calls
     4 ms ResolveNonMSBuildProjectOutput           1 calls
     5 ms CreateCSharpManifestResourceName         1 calls
     5 ms GetFrameworkPath                         1 calls
    10 ms CategorizeSilverlightReferences          1 calls
    11 ms CallTarget                               1 calls
    11 ms FileClassifier                           1 calls
    11 ms FindUnderPath                            5 calls
    11 ms MakeDir                                  1 calls
    13 ms Copy                                     2 calls
    17 ms GetSilverlightFrameworkPath              1 calls
    17 ms RemoveDuplicates                         2 calls
    30 ms AssignProjectConfiguration               1 calls
    32 ms Message                                  25 calls
   239 ms ResolveAssemblyReference                 1 calls
   351 ms MSBuild                                  2 calls
   687 ms CompileXaml                              1 calls
  1413 ms ValidateXaml                             1 calls

Здесь мы говорим о точно таком же решении!

Наконец, вот сценарии, которые я использовал для создания решения:

msbuild:

@setlocal

set SHELFSET=msbuild

set MSBUILDLOGVERBOSERARSEARCHRESULTS=true
set AppConfig=app.config
set Disable_CopyWebApplication=true
set MvcBuildViews=false

call \tmp\undo.cmd
del /a:-R /s/q *.*
tf unshelve %SHELFSET% /recursive /noprompt

msbuild DataSvc.sln
msbuild Main.sln /v:diag > \tmp\00.Main.msbuild.full.log
msbuild Main.sln /v:diag > \tmp\01.Main.msbuild.incr.log
msbuild Main.sln /v:diag > \tmp\02.Main.msbuild.incr.log

@endlocal

devenv:

@setlocal

set SHELFSET=msbuild

set MSBUILDLOGVERBOSERARSEARCHRESULTS=true
set AppConfig=app.config
set Disable_CopyWebApplication=true
set MvcBuildViews=false

call \tmp\undo.cmd
del /a:-R /s/q *.*
tf unshelve %SHELFSET% /recursive /noprompt

msbuild DataSvc.sln
devenv  Main.sln /build > \tmp\00.Main.devenv.full.log
devenv  Main.sln /build > \tmp\01.Main.devenv.incr.log
devenv  Main.sln /build > \tmp\02.Main.devenv.incr.log

@endlocal

Мои тесты говорят мне, что msbuild - это мусор, и я никогда не должен использовать его в командной строке для создания своих решений на C #. https://connect.microsoft.com/VisualStudio/feedback/details/586358/msbuild-ignores-projectsection-projectdependencies-in-sln-file-and-attempts-to-build-projects-in-wrong-order усиливает это ощущение.

Но, возможно, я все-таки ошибаюсь, и простая настройка сделает msbuild такой же эффективной во второй сборке, как devenv.

Есть идеи, как заставить msbuild вести себя нормально при второй сборке?

ИЗМЕНИТЬ 1

Задача CompileXaml является частью цели MarkupCompilePass1, находящейся в C: \ Program Files (x86) \ MSBuild \ Microsoft \ Silverlight \ v5.0 \ Microsoft.Silverlight.Common.targets:

<Target Name="MarkupCompilePass1"
      DependsOnTargets="$(CompileXamlDependsOn)" 
      Condition="'@(Page)@(ApplicationDefinition)' != '' " >

    <CompileXaml 
           LanguageSourceExtension="$(DefaultLanguageSourceExtension)"
           Language="$(Language)" 
           SilverlightPages="@(Page)" 
           SilverlightApplications="@(ApplicationDefinition)" 
           ProjectPath="$(MSBuildProjectFullPath)"
           RootNamespace="$(RootNamespace)"
           AssemblyName="$(AssemblyName)" 
           OutputPath="$(IntermediateOutputPath)"
           SkipLoadingAssembliesInXamlCompiler="$(SkipLoadingAssembliesInXamlCompiler)"
           TargetFrameworkDirectory="$(TargetFrameworkDirectory)" 
           TargetFrameworkSDKDirectory="$(TargetFrameworkSDKDirectory)"
           ReferenceAssemblies ="@(ReferencePath);@(InferredReference->'$(TargetFrameworkDirectory)\%(Identity)')"
           >

        <Output ItemName="Compile" TaskParameter="GeneratedCodeFiles" />

        <!-- Add to the list list of files written. It is used in Microsoft.Common.Targets to clean up 
             for a next clean build 
          -->
        <Output ItemName="FileWrites" TaskParameter="GeneratedCodeFiles" />
        <Output ItemName="_GeneratedCodeFiles" TaskParameter="GeneratedCodeFiles" />

    </CompileXaml>

    <Message Text="(Out) GeneratedCodeFiles: '@(_GeneratedCodeFiles)'" Condition="'$(MSBuildTargetsVerbose)'=='true'"/>
</Target>

Как видим - ни входов, ни выходов.

Далее журнал diag msbuild для второй сборки не содержит подозрительных слов типа «восстановление».

Наконец, я хотел бы отметить, что и msbuild, и devenv выполнялись в абсолютно одинаковых условиях, и ни одна из них не использовала многопоточную сборку. Но разница мизерная - более 5 минут (msbuild) против 3 секунд (devenv, командная строка).

По-прежнему для меня полная загадка.

ИЗМЕНИТЬ 2

Теперь я знаю больше о том, как работает devenv build. Он использует эвристику, чтобы определить, нужно ли в первую очередь передать текущий проект в msbuild. Эта эвристика включена по умолчанию, но ее можно отключить, задав для свойства DisableFastUpToDateCheck msbuild значение true.

Теперь сборке devenv из командной строки требуется более 3 секунд, чтобы выяснить, нужно ли запускать msbuild или нет. В целом для решения, подобного моему, может потребоваться 20 или даже 30 секунд, чтобы решить, что ничего не нужно передавать в msbuild.

Эта эвристика - единственная причина такой огромной разницы во времени. Я предполагаю, что команда Visual Studio признала низкое качество стандартных сценариев сборки (где такие задачи, как MarkupCompilePass1 не управляются входами и выходами), и решила в первую очередь придумать способ пропустить msbuild.

Но есть загвоздка - эвристика проверяет только файл csproj, ни один из импортированных целевых файлов не проверяется. Кроме того, он ничего не знает о неявных зависимостях, таких как файлы TypeScript, на которые ссылаются другие файлы TypeScript. Итак, если ваши файлы TypeScript ссылаются на другие файлы TypeScript, которые принадлежат другому проекту и не связаны явно из файла проекта, эвристика не знает о них, и вам лучше иметь DisableFastUpToDateCheck = true. Сборка будет медленнее, но, по крайней мере, будет правильной.

Итог - я не знаю, как исправить msbuild и, видимо, ребята из devenv тоже. Похоже, это причина того, что они изобрели эвристику.


person mark    schedule 28.03.2014    source источник
comment
Много голосов (и у меня), но мало ответов, я испытываю примерно то же поведение, что и вы, нашли ли вы решение тем временем?   -  person Alessandro Teruzzi    schedule 09.08.2017
comment
См. Мой РЕДАКТИРОВАТЬ 2.   -  person mark    schedule 09.08.2017
comment
Спасибо за обновления   -  person Alessandro Teruzzi    schedule 09.08.2017


Ответы (1)


Во-первых, взгляните на тот журнал диагностики, который вы создаете. На самом деле, сначала используйте средство ведения журнала файлов, а не операторы консоли, чтобы передать вывод консоли в журнал, а затем посмотрите их журналы!

На самом деле вместо /v:diag >msbuild.log используйте это:

/v:min /fl3 /flp3:warningsonly;logfile=msbuild.wrn /fl4 /flp4:errorsOnly;logfile=msbuild.err /fl5 /flp5:Verbosity=diag;logfile=msbuild.log

Теперь ваш консольный буфер благодарит вас, и ваши разработчики тоже благодарит вас за предусмотрительность по ведению отдельных журналов только ошибок и предупреждений для отладки.

Теперь проверьте этот диагностический журнал MsBuild и нажмите CTRL + F на цели, выполнение которых требует много времени. Вы видите какое-нибудь словоблудие, которое означает, что цель снова запущена, хотя ничего не изменилось? Чтобы пропустить сборку, у цели должны быть определенные входы и выходы. Если входы (.cs) новее, чем выходы (.dll, .pdb), то он знает, что что-то изменилось, и запускает новую сборку

Эти цели CompileXaml, которые, как я полагаю, находятся в целях WinFx и имеют определенные входы и выходы, можете ли вы найти выходной текст для одного из этих длительных случаев и определить, привела ли ошибка к его перестройке? В нем говорится: «Полностью перестроить X, потому что Y не может быть найден»?

Наконец, вот забавный трюк, чтобы ускорить сборку из командной строки!

msbuild.exe / м

Это позволит строить проекты отдельно по нескольким потокам.

person Nicodemeus    schedule 28.03.2014
comment
Я добавил правку в свой пост, чтобы ответить на поднятые вами вопросы. - person mark; 30.03.2014