Skip to content

Commit 4d3951c

Browse files
committed
Add performance summary
* Reduced default log output, http get/push calls will now only be shown on verbose mode. * Performance summary displays where time was spent during push operations. * Push batch support. Instead of pushing possibly hundreds of thousands of packages at once push will now load nupkgs in batches and process them to avoid running out of memory. * Files are now ordered during upload. Index files will be pushed last to help avoid conflicts on the client when the feed is still incomplete. * Increased the delay for obtaining a file lock on azure feeds. Cleaned up file lock logging.
1 parent 6b96431 commit 4d3951c

36 files changed

+976
-345
lines changed

ReleaseNotes.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 2.3.73
4+
* Reduced default log output, http get/push calls will now only be shown on verbose mode.
5+
* Performance summary displays where time was spent during push operations.
6+
* Push batch support. Instead of pushing possibly hundreds of thousands of packages at once push will now load nupkgs in batches and process them to avoid running out of memory.
7+
* Files are now ordered during upload. Index files will be pushed last to help avoid conflicts on the client when the feed is still incomplete.
8+
* Increased the delay for obtaining a file lock on azure feeds. Cleaned up file lock logging.
9+
310
## 2.3.36
411
* Path property in sleet.json can now be a relative path for local feeds
512

build/config.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<RepositoryUrl>https://github.com/emgarten/Sleet</RepositoryUrl>
2525
<PackageTags>nuget nugetserver nugetfeed</PackageTags>
2626
<Description>Sleet creates nuget v3.0.0 nupkg feeds.</Description>
27-
<Copyright>Copyright © 2017 Justin Emgarten</Copyright>
27+
<Copyright>Copyright © 2019 Justin Emgarten</Copyright>
2828
<PackageIconUrl>https://emgartenstatic.blob.core.windows.net/nupkgs/icons/sleet.png</PackageIconUrl>
2929
</PropertyGroup>
3030

src/Sleet/DeleteAppCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private static void Run(CommandLineApplication cmd, ILogger log)
5050
Util.SetVerbosity(log, verbose.HasValue());
5151

5252
// Create a temporary folder for caching files during the operation.
53-
using (var cache = new LocalCache())
53+
using (var cache = new LocalCache(new PerfTracker()))
5454
{
5555
// Load settings and file system.
5656
var settings = LocalSettings.Load(optionConfigFile.Value(), SettingsUtility.GetPropertyMappings(propertyOptions.Values));

src/Sleet/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class Program
1010
{
1111
public static int Main(string[] args)
1212
{
13-
var logLevel = LogLevel.Minimal;
13+
var logLevel = Util.DefaultLogLevel;
1414

1515
if (CmdUtils.IsDebugModeEnabled())
1616
{

src/Sleet/PushAppCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private static void Run(CommandLineApplication cmd, ILogger log)
5454
Util.SetVerbosity(log, verbose.HasValue());
5555

5656
// Create a temporary folder for caching files during the operation.
57-
using (var cache = new LocalCache())
57+
using (var cache = new LocalCache(new PerfTracker()))
5858
{
5959
// Load settings and file system.
6060
var settings = LocalSettings.Load(optionConfigFile.Value(), SettingsUtility.GetPropertyMappings(propertyOptions.Values));

src/Sleet/Sleet.nuspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<licenseUrl>https://github.com/emgarten/Sleet/blob/master/LICENSE.md</licenseUrl>
1010
<projectUrl>https://github.com/emgarten/Sleet/</projectUrl>
1111
<description>Sleet is a static feed generator for NuGet package feeds.</description>
12-
<copyright>Copyright © 2017 Justin Emgarten</copyright>
12+
<copyright>Copyright © 2019 Justin Emgarten</copyright>
1313
<tags>nuget nugetserver nugetfeed</tags>
1414
<repository type="git" url="https://github.com/emgarten/Sleet" branch="$branch$" commit="$commit$" />
1515
<developmentDependency>true</developmentDependency>

src/Sleet/Util.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace Sleet
77
{
88
internal static class Util
99
{
10+
internal static LogLevel DefaultLogLevel = LogLevel.Information;
11+
1012
internal static ISleetFileSystem CreateFileSystemOrThrow(LocalSettings settings, string sourceName, LocalCache cache)
1113
{
1214
var sourceNamePassed = !string.IsNullOrEmpty(sourceName);
@@ -83,7 +85,7 @@ internal static void SetVerbosity(ILogger log, bool verbose)
8385
}
8486
else
8587
{
86-
consoleLogger.VerbosityLevel = LogLevel.Information;
88+
consoleLogger.VerbosityLevel = DefaultLogLevel;
8789
consoleLogger.CollapseMessages = true;
8890
}
8991
}

src/SleetLib/Commands/PushCommand.cs

+87-39
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,70 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.IO.Compression;
54
using System.Linq;
65
using System.Threading;
76
using System.Threading.Tasks;
87
using NuGet.Common;
9-
using NuGet.Packaging;
10-
using NuGet.Packaging.Core;
118

129
namespace Sleet
1310
{
1411
public static class PushCommand
1512
{
13+
public const int DefaultBatchSize = 4096;
14+
1615
public static async Task<bool> RunAsync(LocalSettings settings, ISleetFileSystem source, List<string> inputs, bool force, bool skipExisting, ILogger log)
1716
{
1817
var token = CancellationToken.None;
1918
var now = DateTimeOffset.UtcNow;
20-
var packages = new List<PackageInput>();
19+
var success = false;
20+
var perfTracker = source.LocalCache.PerfTracker;
2121

2222
await log.LogAsync(LogLevel.Minimal, $"Reading feed {source.BaseURI.AbsoluteUri}");
2323

24-
// Read packages before locking the feed.
25-
packages.AddRange(await GetPackageInputs(inputs, now, log));
26-
27-
// Check if already initialized
28-
using (var feedLock = await SourceUtility.VerifyInitAndLock(settings, source, log, token))
24+
using (var timer = PerfEntryWrapper.CreateSummaryTimer("Total execution time: {0}", perfTracker))
2925
{
30-
// Validate source
31-
await SourceUtility.ValidateFeedForClient(source, log, token);
26+
// Partition package inputs to avoid reading 100K nuspecs at the same time.
27+
var packagePaths = GetPackagePaths(inputs);
28+
var inputBatches = packagePaths.Partition(DefaultBatchSize);
29+
ISleetFileSystemLock feedLock = null;
3230

33-
// Push
34-
return await PushPackages(settings, source, packages, force, skipExisting, log, token);
31+
try
32+
{
33+
for (var i = 0; i < inputBatches.Count; i++)
34+
{
35+
var inputBatch = inputBatches[i];
36+
if (inputBatches.Count > 1)
37+
{
38+
await log.LogAsync(LogLevel.Minimal, $"Pushing {inputBatch.Count} packages. Batch: {i+1} / {inputBatches.Count}");
39+
}
40+
41+
// Read packages before locking the feed the first time.
42+
var packages = new List<PackageInput>(await GetPackageInputs(inputBatch, now, perfTracker, log));
43+
44+
if (feedLock == null)
45+
{
46+
// Check if already initialized
47+
feedLock = await SourceUtility.VerifyInitAndLock(settings, source, log, token);
48+
49+
// Validate source
50+
await SourceUtility.ValidateFeedForClient(source, log, token);
51+
}
52+
53+
// Push
54+
success = await PushPackages(settings, source, packages, force, skipExisting, log, token);
55+
}
56+
}
57+
finally
58+
{
59+
// Unlock the feed
60+
feedLock?.Dispose();
61+
}
3562
}
63+
64+
// Write out perf summary
65+
await perfTracker.LogSummary(log);
66+
67+
return success;
3668
}
3769

3870
/// <summary>
@@ -42,13 +74,23 @@ public static async Task<bool> RunAsync(LocalSettings settings, ISleetFileSystem
4274
public static async Task<bool> PushPackages(LocalSettings settings, ISleetFileSystem source, List<string> inputs, bool force, bool skipExisting, ILogger log, CancellationToken token)
4375
{
4476
var now = DateTimeOffset.UtcNow;
45-
var packages = new List<PackageInput>();
77+
var success = true;
4678

47-
// Get packages
48-
packages.AddRange(await GetPackageInputs(inputs, now, log));
79+
var packagePaths = GetPackagePaths(inputs);
4980

50-
// Add packages
51-
return await PushPackages(settings, source, packages, force, skipExisting, log, token);
81+
// Partition input files to avoid reading 100K nuspec files at once.
82+
foreach (var inputSegment in packagePaths.Partition(DefaultBatchSize))
83+
{
84+
var packages = new List<PackageInput>();
85+
86+
// Get packages
87+
packages.AddRange(await GetPackageInputs(inputSegment, now, source.LocalCache.PerfTracker, log));
88+
89+
// Add packages
90+
success &= await PushPackages(settings, source, packages, force, skipExisting, log, token);
91+
}
92+
93+
return success;
5294
}
5395

5496
/// <summary>
@@ -74,10 +116,11 @@ public static async Task<bool> PushPackages(LocalSettings settings, ISleetFileSy
74116
SourceSettings = sourceSettings,
75117
Log = log,
76118
Source = source,
77-
Token = token
119+
Token = token,
120+
PerfTracker = source.LocalCache.PerfTracker
78121
};
79122

80-
await log.LogAsync(LogLevel.Information, "Reading existing package index");
123+
await log.LogAsync(LogLevel.Verbose, "Reading existing package index");
81124

82125
var packageIndex = new PackageIndex(context);
83126
await PushPackages(packages, context, packageIndex, force, skipExisting, log);
@@ -122,9 +165,6 @@ private static async Task PushPackages(List<PackageInput> packageInputs, SleetCo
122165
}
123166
}
124167

125-
await log.LogAsync(LogLevel.Minimal, $"Pushing {packageString}");
126-
await log.LogAsync(LogLevel.Information, $"Checking if package exists.");
127-
128168
var exists = false;
129169

130170
if (package.IsSymbolsPackage)
@@ -140,26 +180,29 @@ private static async Task PushPackages(List<PackageInput> packageInputs, SleetCo
140180
{
141181
if (skipExisting)
142182
{
143-
await log.LogAsync(LogLevel.Minimal, $"Package already exists, skipping {packageString}");
183+
await log.LogAsync(LogLevel.Minimal, $"Skip exisiting package: {packageString}");
144184
continue;
145185
}
146186
else if (force)
147187
{
148188
toRemove.Add(package);
149-
await log.LogAsync(LogLevel.Information, $"Package already exists, removing {packageString}");
189+
await log.LogAsync(LogLevel.Information, $"Replace existing package: {packageString}");
150190
}
151191
else
152192
{
153193
throw new InvalidOperationException($"Package already exists: {packageString}.");
154194
}
155195
}
196+
else
197+
{
198+
await log.LogAsync(LogLevel.Minimal, $"Add new package: {packageString}");
199+
}
156200

157201
// Add to list of packages to push
158202
toAdd.Add(package);
159-
await log.LogAsync(LogLevel.Information, $"Adding {packageString}");
160203
}
161204

162-
await log.LogAsync(LogLevel.Minimal, $"Syncing feed files and modifying them locally");
205+
await log.LogAsync(LogLevel.Minimal, $"Processing feed changes");
163206

164207
// Add/Remove packages
165208
var changeContext = SleetOperations.Create(existingPackageSets, toAdd, toRemove);
@@ -169,7 +212,18 @@ private static async Task PushPackages(List<PackageInput> packageInputs, SleetCo
169212
/// <summary>
170213
/// Parse input arguments for nupkg paths.
171214
/// </summary>
172-
private static async Task<List<PackageInput>> GetPackageInputs(List<string> inputs, DateTimeOffset now, ILogger log)
215+
private static async Task<List<PackageInput>> GetPackageInputs(List<string> packagePaths, DateTimeOffset now, IPerfTracker perfTracker, ILogger log)
216+
{
217+
using (var timer = PerfEntryWrapper.CreateSummaryTimer("Loaded package nuspecs in {0}", perfTracker))
218+
{
219+
var tasks = packagePaths.Select(e => new Func<Task<PackageInput>>(() => GetPackageInput(e, log)));
220+
var packageInputs = await TaskUtils.RunAsync(tasks, useTaskRun: true, token: CancellationToken.None);
221+
var packagesSorted = packageInputs.OrderBy(e => e).ToList();
222+
return packagesSorted;
223+
}
224+
}
225+
226+
private static List<string> GetPackagePaths(List<string> inputs)
173227
{
174228
// Check inputs
175229
if (inputs.Count < 1)
@@ -178,15 +232,9 @@ private static async Task<List<PackageInput>> GetPackageInputs(List<string> inpu
178232
}
179233

180234
// Get package inputs
181-
var packagePaths = inputs.SelectMany(GetFiles)
182-
.Distinct(PathUtility.GetStringComparerBasedOnOS())
183-
.ToList();
184-
185-
var tasks = packagePaths.Select(e => new Func<Task<PackageInput>>(() => GetPackageInput(e, log)));
186-
var packageInputs = await TaskUtils.RunAsync(tasks, useTaskRun: true, token: CancellationToken.None);
187-
var packagesSorted = packageInputs.OrderBy(e => e).ToList();
188-
189-
return packagesSorted;
235+
return inputs.SelectMany(GetFiles)
236+
.Distinct(PathUtility.GetStringComparerBasedOnOS())
237+
.ToList();
190238
}
191239

192240
private static void CheckForDuplicates(List<PackageInput> packages)
@@ -206,7 +254,7 @@ private static void CheckForDuplicates(List<PackageInput> packages)
206254
private static Task<PackageInput> GetPackageInput(string file, ILogger log)
207255
{
208256
// Validate package
209-
log.LogInformation($"Reading {file}");
257+
log.LogVerbose($"Reading {file}");
210258
PackageInput packageInput = null;
211259

212260
try
@@ -219,7 +267,7 @@ private static Task<PackageInput> GetPackageInput(string file, ILogger log)
219267
log.LogError($"Invalid package '{file}'.");
220268
throw;
221269
}
222-
270+
223271
// Display a message for non-normalized packages
224272
if (packageInput.Identity.Version.ToString() != packageInput.Identity.Version.ToNormalizedString())
225273
{
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
6+
namespace Sleet
7+
{
8+
/// <summary>
9+
/// Order files in a way that helps avoid clients discovering packages from index files
10+
/// before the package files have been uploaded.
11+
///
12+
/// Order of upload:
13+
/// 1) Nupkg/nuspec files, these are usually the largest and slowest files
14+
/// 2) Misc files such as package details pages and catalog entry pages.
15+
/// 3) index.json files
16+
/// 4) Search page
17+
///
18+
/// </summary>
19+
public class SleetFileComparer : IComparer<ISleetFile>
20+
{
21+
public int Compare(ISleetFile x, ISleetFile y)
22+
{
23+
if (x == null)
24+
{
25+
throw new ArgumentNullException(nameof(x));
26+
}
27+
28+
if (y == null)
29+
{
30+
throw new ArgumentNullException(nameof(y));
31+
}
32+
33+
var priX = GetPriority(x);
34+
var priY = GetPriority(y);
35+
36+
if (priX < priY)
37+
{
38+
return -1;
39+
}
40+
41+
if (priX > priY)
42+
{
43+
return 1;
44+
}
45+
46+
// Fallback to string compare
47+
return StringComparer.OrdinalIgnoreCase.Compare(x.EntityUri.AbsoluteUri, y.EntityUri.AbsoluteUri);
48+
}
49+
50+
private static int GetPriority(ISleetFile file)
51+
{
52+
var fileName = file.EntityUri.AbsoluteUri.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Last().ToLowerInvariant();
53+
54+
if (!string.IsNullOrEmpty(fileName))
55+
{
56+
if (fileName.IndexOf('.') > -1)
57+
{
58+
var extension = Path.GetExtension(fileName).ToLowerInvariant();
59+
switch (extension)
60+
{
61+
case "nupkg":
62+
return 1;
63+
case "nuspec":
64+
return 2;
65+
}
66+
}
67+
68+
switch (fileName)
69+
{
70+
case "index.json":
71+
// Upload index.json files last after everything else is in place
72+
return 7;
73+
case "query":
74+
// Update search after index files
75+
return 8;
76+
}
77+
}
78+
79+
return 5;
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)