forked from npgsql/npgsql
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPreparedStatementManager.cs
More file actions
216 lines (192 loc) · 8.82 KB
/
PreparedStatementManager.cs
File metadata and controls
216 lines (192 loc) · 8.82 KB
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
28
29
30
31
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Npgsql.Logging;
namespace Npgsql
{
class PreparedStatementManager
{
internal int MaxAutoPrepared { get; }
internal int UsagesBeforePrepare { get; }
internal Dictionary<string, PreparedStatement> BySql { get; } = new Dictionary<string, PreparedStatement>();
readonly PreparedStatement[] _autoPrepared;
int _numAutoPrepared;
readonly PreparedStatement?[] _candidates;
/// <summary>
/// Total number of current prepared statements (whether explicit or automatic).
/// </summary>
internal int NumPrepared;
readonly NpgsqlConnector _connector;
internal string NextPreparedStatementName() => "_p" + (++_preparedStatementIndex);
ulong _preparedStatementIndex;
static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(PreparedStatementManager));
internal const int CandidateCount = 100;
internal PreparedStatementManager(NpgsqlConnector connector)
{
_connector = connector;
MaxAutoPrepared = connector.Settings.MaxAutoPrepare;
UsagesBeforePrepare = connector.Settings.AutoPrepareMinUsages;
if (MaxAutoPrepared > 0)
{
if (MaxAutoPrepared > 256)
Log.Warn($"{nameof(MaxAutoPrepared)} is over 256, performance degradation may occur. Please report via an issue.", connector.Id);
_autoPrepared = new PreparedStatement[MaxAutoPrepared];
_candidates = new PreparedStatement[CandidateCount];
}
else
{
_autoPrepared = null!;
_candidates = null!;
}
}
internal PreparedStatement? GetOrAddExplicit(NpgsqlStatement statement)
{
var sql = statement.SQL;
PreparedStatement? statementBeingReplaced = null;
if (BySql.TryGetValue(sql, out var pStatement))
{
Debug.Assert(pStatement.State != PreparedState.Unprepared);
if (pStatement.IsExplicit)
{
// Great, we've found an explicit prepared statement.
// We just need to check that the parameter types correspond, since prepared statements are
// only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
return pStatement.DoParametersMatch(statement.InputParameters)
? pStatement
: null;
}
// We've found an autoprepare statement (candidate or otherwise)
switch (pStatement.State)
{
case PreparedState.NotPrepared:
// Found a candidate for autopreparation. Remove it and prepare explicitly.
RemoveCandidate(pStatement);
break;
case PreparedState.Prepared:
// The statement has already been autoprepared. We need to "promote" it to explicit.
statementBeingReplaced = pStatement;
break;
case PreparedState.Unprepared:
throw new InvalidOperationException($"Found unprepared statement in {nameof(PreparedStatementManager)}");
default:
throw new ArgumentOutOfRangeException();
}
}
// Statement hasn't been prepared yet
return BySql[sql] = PreparedStatement.CreateExplicit(this, sql, NextPreparedStatementName(), statement.InputParameters, statementBeingReplaced);
}
internal PreparedStatement? TryGetAutoPrepared(NpgsqlStatement statement)
{
var sql = statement.SQL;
if (!BySql.TryGetValue(sql, out var pStatement))
{
// New candidate. Find an empty candidate slot or eject a least-used one.
int slotIndex = -1, leastUsages = int.MaxValue;
var lastUsed = DateTime.MaxValue;
for (var i = 0; i < _candidates.Length; i++)
{
var candidate = _candidates[i];
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
// ReSharper disable HeuristicUnreachableCode
if (candidate == null) // Found an unused candidate slot, return immediately
{
slotIndex = i;
break;
}
// ReSharper restore HeuristicUnreachableCode
if (candidate.Usages < leastUsages)
{
leastUsages = candidate.Usages;
slotIndex = i;
lastUsed = candidate.LastUsed;
}
else if (candidate.Usages == leastUsages && candidate.LastUsed < lastUsed)
{
slotIndex = i;
lastUsed = candidate.LastUsed;
}
}
var leastUsed = _candidates[slotIndex];
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (leastUsed != null)
BySql.Remove(leastUsed.Sql);
pStatement = BySql[sql] = _candidates[slotIndex] = PreparedStatement.CreateAutoPrepareCandidate(this, sql);
}
switch (pStatement.State)
{
case PreparedState.Prepared:
case PreparedState.BeingPrepared:
// The statement has already been prepared (explicitly or automatically), or has been selected
// for preparation (earlier identical statement in the same command).
// We just need to check that the parameter types correspond, since prepared statements are
// only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
return pStatement.DoParametersMatch(statement.InputParameters)
? pStatement
: null;
}
if (++pStatement.Usages < UsagesBeforePrepare)
{
// Statement still hasn't passed the usage threshold, no automatic preparation.
// Return null for unprepared execution.
pStatement.LastUsed = DateTime.UtcNow;
return null;
}
// Bingo, we've just passed the usage threshold, statement should get prepared
Log.Trace($"Automatically preparing statement: {sql}", _connector.Id);
RemoveCandidate(pStatement);
if (_numAutoPrepared < MaxAutoPrepared)
{
// We still have free slots
_autoPrepared[_numAutoPrepared++] = pStatement;
pStatement.Name = "_auto" + _numAutoPrepared;
}
else
{
// We already have the maximum number of prepared statements.
// Find the least recently used prepared statement and replace it.
var oldestTimestamp = DateTime.MaxValue;
var oldestIndex = -1;
for (var i = 0; i < _autoPrepared.Length; i++)
{
if (_autoPrepared[i].LastUsed < oldestTimestamp)
{
oldestIndex = i;
oldestTimestamp = _autoPrepared[i].LastUsed;
}
}
var lru = _autoPrepared[oldestIndex];
pStatement.Name = lru.Name;
pStatement.StatementBeingReplaced = lru;
_autoPrepared[oldestIndex] = pStatement;
}
// Note that the parameter types are only set at the moment of preparation - in the candidate phase
// there's no differentiation between overloaded statements, which are a pretty rare case, saving
// allocations.
pStatement.SetParamTypes(statement.InputParameters);
return pStatement;
}
void RemoveCandidate(PreparedStatement candidate)
{
var i = 0;
for (; i < _candidates.Length; i++)
{
if (_candidates[i] == candidate)
{
_candidates[i] = null;
return;
}
}
Debug.Assert(i < _candidates.Length);
}
internal void ClearAll()
{
BySql.Clear();
NumPrepared = 0;
_preparedStatementIndex = 0;
_numAutoPrepared = 0;
if (_candidates != null)
for (var i = 0; i < _candidates.Length; i++)
_candidates[i] = null;
}
}
}