Skip to content

Commit 3df003b

Browse files
authored
Use OPTIMIZE FOR UNKNOWN for dependents queries (#8112)
* Use OPTIMIZE FOR UNKNOWN for dependents queries * Add query hint configuration to RECOMPILE for some package IDs Progress on #8078
1 parent 5caec43 commit 3df003b

16 files changed

Lines changed: 183 additions & 11 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace NuGetGallery.Services
5+
{
6+
public interface IQueryHintConfiguration
7+
{
8+
/// <summary>
9+
/// Determines whether the RECOMPILE query hint should be used for the package dependents query. Some popular
10+
/// package IDs perform much better with their own dedicated query plan instead of OPTIMIZE FOR UNKNOWN.
11+
/// </summary>
12+
/// <param name="packageId">The package ID.</param>
13+
/// <returns>True, if the RECOMPILE query hint should be used for the provided package ID.</returns>
14+
bool ShouldUseRecompileForPackageDependents(string packageId);
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Newtonsoft.Json;
8+
9+
namespace NuGetGallery.Services
10+
{
11+
public class QueryHintConfiguration : IQueryHintConfiguration
12+
{
13+
public QueryHintConfiguration() : this(Enumerable.Empty<string>())
14+
{
15+
}
16+
17+
[JsonConstructor]
18+
public QueryHintConfiguration(IEnumerable<string> recompileForPackageDependents)
19+
{
20+
RecompileForPackageDependents = new HashSet<string>(
21+
recompileForPackageDependents ?? Enumerable.Empty<string>(),
22+
StringComparer.OrdinalIgnoreCase);
23+
}
24+
25+
public HashSet<string> RecompileForPackageDependents { get; }
26+
27+
public bool ShouldUseRecompileForPackageDependents(string packageId)
28+
{
29+
return RecompileForPackageDependents.Contains(packageId);
30+
}
31+
}
32+
}

src/NuGetGallery.Services/NuGetGallery.Services.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,14 @@
116116
<Compile Include="Configuration\ILoginDiscontinuationConfiguration.cs" />
117117
<Compile Include="Configuration\IPackageDeleteConfiguration.cs" />
118118
<Compile Include="Configuration\IODataCacheConfiguration.cs" />
119+
<Compile Include="Configuration\IQueryHintConfiguration.cs" />
119120
<Compile Include="Configuration\IServiceBusConfiguration.cs" />
120121
<Compile Include="Configuration\ISymbolsConfiguration.cs" />
121122
<Compile Include="Configuration\ITyposquattingConfiguration.cs" />
122123
<Compile Include="Configuration\LoginDiscontinuationConfiguration.cs" />
123124
<Compile Include="Configuration\ODataCacheConfiguration.cs" />
124125
<Compile Include="Configuration\PackageDeleteConfiguration.cs" />
126+
<Compile Include="Configuration\QueryHintConfiguration.cs" />
125127
<Compile Include="Configuration\SecretReader\EmptySecretReaderFactory.cs" />
126128
<Compile Include="Configuration\SecretReader\SecretReaderFactory.cs" />
127129
<Compile Include="Configuration\ServiceBusConfiguration.cs" />

src/NuGetGallery.Services/PackageManagement/PackageService.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Data.Common;
76
using System.Data.Entity;
87
using System.IO;
98
using System.Linq;
@@ -26,6 +25,7 @@ public class PackageService : CorePackageService, IPackageService
2625
private readonly ITelemetryService _telemetryService;
2726
private readonly ISecurityPolicyService _securityPolicyService;
2827
private readonly IEntitiesContext _entitiesContext;
28+
private readonly IContentObjectService _contentObjectService;
2929
private const int packagesDisplayed = 5;
3030

3131
public PackageService(
@@ -35,13 +35,15 @@ public PackageService(
3535
IAuditingService auditingService,
3636
ITelemetryService telemetryService,
3737
ISecurityPolicyService securityPolicyService,
38-
IEntitiesContext entitiesContext)
38+
IEntitiesContext entitiesContext,
39+
IContentObjectService contentObjectService)
3940
: base(packageRepository, packageRegistrationRepository, certificateRepository)
4041
{
4142
_auditingService = auditingService ?? throw new ArgumentNullException(nameof(auditingService));
4243
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
4344
_securityPolicyService = securityPolicyService ?? throw new ArgumentNullException(nameof(securityPolicyService));
4445
_entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
46+
_contentObjectService = contentObjectService ?? throw new ArgumentNullException(nameof(contentObjectService));
4547
}
4648

4749
/// <summary>
@@ -155,8 +157,34 @@ public PackageDependents GetPackageDependents(string id)
155157
}
156158

157159
PackageDependents result = new PackageDependents();
158-
159-
using (_entitiesContext.WithQueryHint("RECOMPILE"))
160+
161+
// We use OPTIMIZE FOR UNKNOWN by default here because there are distinct 2-3 query plans that may be
162+
// selected via SQL Server parameter sniffing. Because SQL Server caches query plans, this means that the
163+
// first parameter plus query combination that SQL sees defines which query plan is selected and cached for
164+
// all subsequent parameter values of the same query. This could result in a non-optimal query plan getting
165+
// cached depending on what package ID is viewed first. Using OPTIMIZE FOR UNKNOWN causes a predictable
166+
// query plan to be cached.
167+
//
168+
// For example, the query plan for Newtonsoft.Json is very good for that specific parameter value since
169+
// there are so many package dependents but the same query plan takes a very long time for packages with few
170+
// or no dependents. The query plan for "UNKNOWN" (that is a package ID with unknown SQL Server statistic)
171+
// behaves somewhat poorly for Newtonsoft.Json (2-5 seconds) but very well for the vast majority of
172+
// packages. Because we have in-memory caching above this layer, OPTIMIZE FOR UNKNOWN is acceptable other
173+
// unconfigured cases similar to Newtonsoft.Json because the extra cost of the non-optimal query plan is
174+
// amortized over many, many page views. For the long tail packages, in-memory caching is less effective
175+
// (low page views) so an optimal query should be selected for this category.
176+
//
177+
// For the cases where RECOMPILE is known to perform the best, the package ID can be added to the query hint
178+
// configuration JSON file from the content object service. This should only be done when the following
179+
// things are true:
180+
//
181+
// 1. The overhead of SQL Server recompile is worth it. We have seen the overhead to be 5-50ms.
182+
// 2. SQL Server has up to date statistics which will lead to the proper query plan being selected.
183+
// 3. SQL Server actually picks the proper query plan. We have observed cases where this does not happen
184+
// even with up-to-date statistics.
185+
//
186+
var useRecompile = _contentObjectService.QueryHintConfiguration.ShouldUseRecompileForPackageDependents(id);
187+
using (_entitiesContext.WithQueryHint(useRecompile ? "RECOMPILE" : "OPTIMIZE FOR UNKNOWN"))
160188
{
161189
result.TopPackages = GetListOfDependents(id);
162190
result.TotalPackageCount = GetDependentCount(id);

src/NuGetGallery.Services/ServicesConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static class ContentNames
5656
public static readonly string ABTestConfiguration = "AB-Test-Configuration";
5757
public static readonly string ODataCacheConfiguration = "OData-Cache-Configuration";
5858
public static readonly string CacheConfiguration = "Cache-Configuration";
59+
public static readonly string QueryHintConfiguration = "Query-Hint-Configuration";
5960
}
6061
}
6162
}

src/NuGetGallery.Services/Storage/ContentObjectService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public ContentObjectService(IContentService contentService)
2727
ABTestConfiguration = new ABTestConfiguration();
2828
ODataCacheConfiguration = new ODataCacheConfiguration();
2929
CacheConfiguration = new CacheConfiguration();
30+
QueryHintConfiguration = new QueryHintConfiguration();
3031
}
3132

3233
public ILoginDiscontinuationConfiguration LoginDiscontinuationConfiguration { get; private set; }
@@ -37,6 +38,7 @@ public ContentObjectService(IContentService contentService)
3738
public IABTestConfiguration ABTestConfiguration { get; private set; }
3839
public IODataCacheConfiguration ODataCacheConfiguration { get; private set; }
3940
public ICacheConfiguration CacheConfiguration { get; private set; }
41+
public IQueryHintConfiguration QueryHintConfiguration { get; private set; }
4042

4143
public async Task Refresh()
4244
{
@@ -72,6 +74,10 @@ await Refresh<ODataCacheConfiguration>(ServicesConstants.ContentNames.ODataCache
7274
CacheConfiguration =
7375
await Refresh<CacheConfiguration>(ServicesConstants.ContentNames.CacheConfiguration) ??
7476
new CacheConfiguration();
77+
78+
QueryHintConfiguration =
79+
await Refresh<QueryHintConfiguration>(ServicesConstants.ContentNames.QueryHintConfiguration) ??
80+
new QueryHintConfiguration();
7581
}
7682

7783
private async Task<T> Refresh<T>(string contentName)

src/NuGetGallery.Services/Storage/IContentObjectService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public interface IContentObjectService
1616
IABTestConfiguration ABTestConfiguration { get; }
1717
IODataCacheConfiguration ODataCacheConfiguration { get; }
1818
ICacheConfiguration CacheConfiguration { get; }
19+
IQueryHintConfiguration QueryHintConfiguration { get; }
1920

2021
Task Refresh();
2122
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"RecompileForPackageDependents": []
3+
}

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,6 +2302,9 @@
23022302
<ItemGroup>
23032303
<Content Include="App_Data\Files\Content\Cache-Configuration.json" />
23042304
</ItemGroup>
2305+
<ItemGroup>
2306+
<Content Include="App_Data\Files\Content\Query-Hint-Configuration.json" />
2307+
</ItemGroup>
23052308
<PropertyGroup>
23062309
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
23072310
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

src/NuGetGallery/Web.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@
360360
</customErrors>
361361
<sessionState mode="Off"/>
362362
<machineKey configProtectionProvider="GalleryMachineKeyConfigurationProvider">
363-
<EncryptedData />
363+
<EncryptedData/>
364364
</machineKey>
365365
</system.web>
366366
<system.webServer>

0 commit comments

Comments
 (0)