Skip to content

Commit d3fe729

Browse files
committed
Improved calculations
1 parent bdbf09c commit d3fe729

5 files changed

Lines changed: 448 additions & 29 deletions

File tree

src/EfCore.StorageEstimator/EfCore.StorageEstimator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<!-- NuGet Package Properties (common properties inherited from Directory.Build.props) -->
77
<PackageId>EfCore.StorageEstimator</PackageId>
8-
<Version>1.0.0</Version>
8+
<Version>1.1.0</Version>
99
<Description>Estimate PostgreSQL storage footprint and database pressure from EF Core models using annotation-driven workload assumptions.</Description>
1010
<PackageTags>efcore;postgresql;storage;estimation;analysis</PackageTags>
1111
</PropertyGroup>

src/EfCore.StorageEstimator/Estimation/PostgreSqlStorageMath.cs

Lines changed: 244 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal static class PostgreSqlStorageMath
1616
private const double IndexPageHeaderBytes = 24;
1717
private const double BTreeSpecialSpaceBytes = 16;
1818
private const double IndexTupleHeaderBytes = 8;
19+
private const double IndexNullBitmapHeaderBytes = 16;
1920
private const double IndexRowPointerBytes = 4;
2021
private const int MaxAlignment = 8;
2122
private const int DefaultVariableLengthBytes = 256;
@@ -107,11 +108,15 @@ private static EntityStorageEstimate EstimateEntity(
107108
.Select(property => EstimateProperty(property, $"{entityPath}.{property.Name}", warnings))
108109
.ToArray();
109110
var propertyLookup = propertyEstimates.ToDictionary(property => property.Name, StringComparer.Ordinal);
110-
var tupleHeaderBytes = Align(
111+
var tupleHeaderBytesWithoutNulls = Align(HeapTupleHeaderBytes, MaxAlignment);
112+
var tupleHeaderBytesWithNulls = Align(
111113
HeapTupleHeaderBytes + GetNullBitmapBytes(properties),
112114
MaxAlignment);
113-
var tupleDataBytes = propertyEstimates.Sum(property => property.AverageStoredBytes);
114-
var averageHeapRowBytes = Align(tupleHeaderBytes + tupleDataBytes, MaxAlignment) + HeapItemPointerBytes;
115+
var averageHeapTupleBytes = EstimateAverageTupleBytes(
116+
tupleHeaderBytesWithoutNulls,
117+
tupleHeaderBytesWithNulls,
118+
propertyEstimates);
119+
var averageHeapRowBytes = averageHeapTupleBytes + HeapItemPointerBytes;
115120
var heapBytes = EstimatePagedBytes(rowCount, averageHeapRowBytes, HeapPageHeaderBytes, 0);
116121
var indexEstimates = indexes
117122
.Select(index => EstimateIndex(index, propertyLookup, rowCount))
@@ -138,7 +143,11 @@ private static PropertySizeEstimate EstimateProperty(
138143
warnings.Add(
139144
$"Using fallback fill rate 1.0 for nullable fixed-width property '{propertyPath}'. Add [{nameof(StorageFieldAttribute)}] to override it.");
140145

141-
return new PropertySizeEstimate(property.Name, size * fillRate);
146+
return new PropertySizeEstimate(
147+
property.Name,
148+
size,
149+
GetFixedAlignment(property, propertyPath, warnings),
150+
fillRate);
142151
}
143152

144153

@@ -184,13 +193,23 @@ private static PropertySizeEstimate EstimateVariableLengthProperty(
184193
}
185194

186195
if (IsNumericType(property.ClrType, property.ProviderClrType, property.StoreType))
187-
return new PropertySizeEstimate(property.Name, averageLength.Value * fillRate);
196+
return new PropertySizeEstimate(
197+
property.Name,
198+
averageLength.Value,
199+
GetVariableLengthAlignment(averageLength.Value),
200+
fillRate);
188201

189202
var overheadBytes = averageLength <= 126
190203
? 1
191204
: 4;
192205

193-
return new PropertySizeEstimate(property.Name, (averageLength.Value + overheadBytes) * fillRate);
206+
var storedBytes = averageLength.Value + overheadBytes;
207+
208+
return new PropertySizeEstimate(
209+
property.Name,
210+
storedBytes,
211+
GetVariableLengthAlignment(storedBytes),
212+
fillRate);
194213
}
195214

196215

@@ -213,8 +232,14 @@ private static StorageIndexEstimate EstimateIndex(
213232
IReadOnlyDictionary<string, PropertySizeEstimate> propertyLookup,
214233
double rowCount)
215234
{
216-
var keyBytes = index.Properties.Sum(property => propertyLookup[property.Name].AverageStoredBytes);
217-
var averageEntryBytes = Align(IndexTupleHeaderBytes + keyBytes, MaxAlignment) + IndexRowPointerBytes;
235+
var keyProperties = index.Properties
236+
.Select(property => propertyLookup[property.Name])
237+
.ToArray();
238+
var averageTupleBytes = EstimateAverageTupleBytes(
239+
IndexTupleHeaderBytes,
240+
IndexNullBitmapHeaderBytes,
241+
keyProperties);
242+
var averageEntryBytes = averageTupleBytes + IndexRowPointerBytes;
218243
var estimatedBytes = EstimatePagedBytes(rowCount, averageEntryBytes, IndexPageHeaderBytes, BTreeSpecialSpaceBytes) + HeapPageBytes;
219244

220245
return new StorageIndexEstimate(
@@ -226,6 +251,143 @@ private static StorageIndexEstimate EstimateIndex(
226251
}
227252

228253

254+
private static double EstimateAverageTupleBytes(
255+
double headerBytesWithoutNulls,
256+
double headerBytesWithNulls,
257+
IReadOnlyList<PropertySizeEstimate> properties)
258+
{
259+
if (properties.Count == 0)
260+
return Align(headerBytesWithoutNulls, MaxAlignment);
261+
262+
var allPresentProbability = properties.Aggregate(1.0d, (current, property) => current * property.FillRate);
263+
var allPresentBytesWithoutNulls = EstimateTupleBytesForAlwaysPresentProperties(headerBytesWithoutNulls, properties);
264+
265+
if (allPresentProbability >= 1.0d)
266+
return allPresentBytesWithoutNulls;
267+
268+
var allPresentBytesWithNulls = EstimateTupleBytesForAlwaysPresentProperties(headerBytesWithNulls, properties);
269+
var expectedBytesWithNullableHeader = EstimateExpectedTupleBytes(
270+
headerBytesWithNulls,
271+
properties);
272+
273+
return (allPresentProbability * allPresentBytesWithoutNulls) +
274+
expectedBytesWithNullableHeader -
275+
(allPresentProbability * allPresentBytesWithNulls);
276+
}
277+
278+
279+
private static double EstimateTupleBytesForAlwaysPresentProperties(
280+
double headerBytes,
281+
IReadOnlyList<PropertySizeEstimate> properties)
282+
{
283+
var offset = headerBytes;
284+
285+
foreach (var property in properties)
286+
{
287+
offset = Align(offset, property.AlignmentBytes);
288+
offset += property.BytesWhenPresent;
289+
}
290+
291+
return Align(offset, MaxAlignment);
292+
}
293+
294+
295+
private static double EstimateExpectedTupleBytes(
296+
double headerBytes,
297+
IReadOnlyList<PropertySizeEstimate> properties)
298+
{
299+
var states = new Dictionary<int, TupleLayoutState>
300+
{
301+
[GetResidue(headerBytes)] = new TupleLayoutState(1.0d, headerBytes)
302+
};
303+
304+
foreach (var property in properties)
305+
{
306+
var nextStates = new Dictionary<int, TupleLayoutState>();
307+
308+
foreach (var state in states)
309+
{
310+
var residue = state.Key;
311+
var layoutState = state.Value;
312+
313+
if (property.FillRate < 1.0d)
314+
AddState(
315+
nextStates,
316+
residue,
317+
new TupleLayoutState(
318+
layoutState.Probability * (1.0d - property.FillRate),
319+
layoutState.WeightedOffsetSum * (1.0d - property.FillRate)));
320+
321+
if (property.FillRate <= 0.0d)
322+
continue;
323+
324+
var paddingBytes = GetPaddingBytes(residue, property.AlignmentBytes);
325+
var addedBytes = paddingBytes + property.BytesWhenPresent;
326+
327+
AddProbability(
328+
nextStates,
329+
GetResidue(residue + addedBytes),
330+
new TupleLayoutState(
331+
layoutState.Probability * property.FillRate,
332+
(layoutState.WeightedOffsetSum * property.FillRate) + (layoutState.Probability * property.FillRate * addedBytes)));
333+
}
334+
335+
states = nextStates;
336+
}
337+
338+
return states.Sum(state => state.Value.WeightedOffsetSum + (state.Value.Probability * GetPaddingBytes(state.Key, MaxAlignment)));
339+
}
340+
341+
342+
private static int GetPaddingBytes(
343+
int residue,
344+
int alignmentBytes)
345+
{
346+
if (alignmentBytes <= 1)
347+
return 0;
348+
349+
var misalignment = residue % alignmentBytes;
350+
351+
return misalignment == 0
352+
? 0
353+
: alignmentBytes - misalignment;
354+
}
355+
356+
357+
private static int GetResidue(double offset)
358+
{
359+
return ((int)Math.Round(offset, MidpointRounding.AwayFromZero)) % MaxAlignment;
360+
}
361+
362+
363+
private static void AddProbability(
364+
IDictionary<int, TupleLayoutState> states,
365+
int residue,
366+
TupleLayoutState state)
367+
{
368+
if (state.Probability <= 0.0d)
369+
return;
370+
371+
AddState(states, residue, state);
372+
}
373+
374+
375+
private static void AddState(
376+
IDictionary<int, TupleLayoutState> states,
377+
int residue,
378+
TupleLayoutState state)
379+
{
380+
if (state.Probability <= 0.0d)
381+
return;
382+
383+
states[residue] = states.TryGetValue(residue, out var current)
384+
? new TupleLayoutState(
385+
current.Probability + state.Probability,
386+
current.WeightedOffsetSum + state.WeightedOffsetSum)
387+
: state;
388+
}
389+
390+
229391
private static double EstimatePagedBytes(
230392
double rowCount,
231393
double averageEntryBytes,
@@ -251,7 +413,7 @@ private static double GetNullBitmapBytes(IReadOnlyList<PropertySizingInput> prop
251413
}
252414

253415

254-
private static double GetFixedSize(
416+
private static int GetFixedSize(
255417
PropertySizingInput property,
256418
string propertyPath,
257419
ICollection<string> warnings)
@@ -323,6 +485,64 @@ private static double GetFixedSize(
323485
}
324486

325487

488+
private static int GetFixedAlignment(
489+
PropertySizingInput property,
490+
string propertyPath,
491+
ICollection<string> warnings)
492+
{
493+
var nonNullableClrType = Nullable.GetUnderlyingType(property.ClrType) ?? property.ClrType;
494+
var nonNullableProviderType = Nullable.GetUnderlyingType(property.ProviderClrType) ?? property.ProviderClrType;
495+
var storeType = property.StoreType ?? string.Empty;
496+
497+
if (nonNullableProviderType.IsEnum)
498+
nonNullableProviderType = Enum.GetUnderlyingType(nonNullableProviderType);
499+
500+
if (nonNullableClrType.IsEnum)
501+
nonNullableClrType = Enum.GetUnderlyingType(nonNullableClrType);
502+
503+
if (nonNullableProviderType == typeof(bool) || nonNullableClrType == typeof(bool) ||
504+
nonNullableProviderType == typeof(byte) || nonNullableProviderType == typeof(sbyte) ||
505+
nonNullableClrType == typeof(byte) || nonNullableClrType == typeof(sbyte) ||
506+
nonNullableProviderType == typeof(Guid) || nonNullableClrType == typeof(Guid) ||
507+
storeType.Contains("uuid", StringComparison.OrdinalIgnoreCase))
508+
return 1;
509+
510+
if (nonNullableProviderType == typeof(short) || nonNullableProviderType == typeof(ushort) ||
511+
nonNullableClrType == typeof(short) || nonNullableClrType == typeof(ushort))
512+
return 2;
513+
514+
if (nonNullableProviderType == typeof(int) || nonNullableProviderType == typeof(uint) ||
515+
nonNullableClrType == typeof(int) || nonNullableClrType == typeof(uint) ||
516+
nonNullableProviderType == typeof(float) || nonNullableClrType == typeof(float) ||
517+
storeType.Contains("date", StringComparison.OrdinalIgnoreCase))
518+
return 4;
519+
520+
if (nonNullableProviderType == typeof(long) || nonNullableProviderType == typeof(ulong) ||
521+
nonNullableClrType == typeof(long) || nonNullableClrType == typeof(ulong) ||
522+
nonNullableProviderType == typeof(double) || nonNullableClrType == typeof(double) ||
523+
nonNullableProviderType == typeof(DateTime) || nonNullableClrType == typeof(DateTime) ||
524+
nonNullableProviderType == typeof(DateTimeOffset) || nonNullableClrType == typeof(DateTimeOffset) ||
525+
nonNullableProviderType == typeof(TimeSpan) || nonNullableClrType == typeof(TimeSpan) ||
526+
storeType.Contains("timestamp", StringComparison.OrdinalIgnoreCase) ||
527+
storeType.Contains("interval", StringComparison.OrdinalIgnoreCase) ||
528+
storeType.Contains("time with time zone", StringComparison.OrdinalIgnoreCase) ||
529+
storeType.Contains("time", StringComparison.OrdinalIgnoreCase))
530+
return 8;
531+
532+
warnings.Add($"Using fallback fixed-size alignment 8 for '{propertyPath}' because its PostgreSQL alignment could not be inferred.");
533+
534+
return 8;
535+
}
536+
537+
538+
private static int GetVariableLengthAlignment(double storedBytes)
539+
{
540+
return storedBytes <= 126
541+
? 1
542+
: 4;
543+
}
544+
545+
326546
private static bool IsNumericType(
327547
Type clrType,
328548
Type providerClrType,
@@ -512,12 +732,25 @@ public StoragePropertySchema ToSchema()
512732

513733
private sealed class PropertySizeEstimate(
514734
string name,
515-
double averageStoredBytes)
735+
double bytesWhenPresent,
736+
int alignmentBytes,
737+
double fillRate)
516738
{
517739
public string Name { get; } = name;
518740

519-
public double AverageStoredBytes { get; } = averageStoredBytes;
741+
public double BytesWhenPresent { get; } = bytesWhenPresent;
742+
743+
public int AlignmentBytes { get; } = alignmentBytes;
744+
745+
public double FillRate { get; } = fillRate;
746+
747+
public double AverageStoredBytes => BytesWhenPresent * FillRate;
520748
}
521749

750+
751+
private readonly record struct TupleLayoutState(
752+
double Probability,
753+
double WeightedOffsetSum);
754+
522755
#endregion
523756
}

src/EfCore.StorageEstimator/Schema/EfCoreSchemaReader.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ public EfCoreEntitySchema GetEntitySchema(IEntityType entityType)
6565
var propertyLookup = properties.ToDictionary(property => property.Name, StringComparer.Ordinal);
6666
var indexes = entityType
6767
.GetIndexes()
68-
.Where(index => !IsPureForeignKeyIndex(entityType, index))
6968
.Select(index => CreateIndexSchema(index, propertyLookup))
7069
.ToArray();
7170
var entitySchema = new EfCoreEntitySchema(
@@ -279,17 +278,6 @@ private static bool IsVariableLength(
279278
storeType.Contains("decimal", StringComparison.OrdinalIgnoreCase);
280279
}
281280

282-
283-
private static bool IsPureForeignKeyIndex(IEntityType entityType, IIndex index)
284-
{
285-
var indexPropertyNames = index.Properties.Select(property => property.Name).ToArray();
286-
287-
return entityType
288-
.GetForeignKeys()
289-
.Any(foreignKey => foreignKey.Properties.Select(property => property.Name).SequenceEqual(indexPropertyNames));
290-
}
291-
292-
293281
private static bool IsScalarCollectionType(Type type)
294282
{
295283
if (type == typeof(string) || type == typeof(byte[]))

0 commit comments

Comments
 (0)