Skip to content

Commit 1509847

Browse files
authored
[DBMON-6338] Optimize StatementMetrics to reduce small memory allocations and GC pressure (DataDog#23075)
* Optimize StatementMetrics to reduce small memory alllocations and GC pressure * cleanup additional unnecessary guard * Fix formatting * Add changelog * Fix changelog
1 parent 48bff03 commit 1509847

2 files changed

Lines changed: 49 additions & 41 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reduce allocations in `StatementMetrics` by deferring dict construction and updating the previous-statements cache in place.

datadog_checks_base/datadog_checks/base/utils/db/statement_metrics.py

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,58 +60,65 @@ def compute_derivative_rows(self, rows, metrics, key, execution_indicators=None)
6060
'Some statement metrics are not available from the table: %s', ','.join(m for m in dropped_metrics)
6161
)
6262

63+
# All rows within a call share the same schema, so metric_columns only
64+
# needs to be computed once from the first (row, prev) pair.
65+
metric_columns = None
66+
indicator_cols = None
67+
6368
for row_key, row in merged_rows.items():
6469
prev = self._previous_statements.get(row_key)
6570
if prev is None:
6671
continue
6772

68-
metric_columns = metrics & row.keys() & prev.keys()
69-
70-
# Take the diff of all metric values between the current row and the previous run's row.
71-
# There are a couple of edge cases to be aware of:
72-
#
73-
# 1. Table truncation or stats reset: Because the table values are always increasing, a negative value
74-
# suggests truncation or a stats reset. In this case, the row difference is discarded and the row should.
75-
# be tracked from this run forward.
76-
#
77-
# 2. No changes since the previous run: There is no need to store metrics of 0, since that is implied by
78-
# the absence of metrics. On any given check run, most rows will have no difference so this optimization
79-
# avoids having to send a lot of unnecessary metrics.
80-
#
81-
# 3. Execution indicators: If execution_indicators is specified, only consider a query as changed if at
82-
# least one of the execution indicator metrics has changed. This helps filter out cases where an old or
83-
# less frequently executed normalized query was evicted due to the stats table being full, and then
84-
# re-inserted to the stats table with a small call count and slight duration change. In this case,
85-
# the new normalized query entry should be treated as the baseline for future diffs.
86-
87-
diffed_row = {k: row[k] - prev[k] if k in metric_columns else row[k] for k in row.keys()}
88-
89-
# Check for negative values, but only in the columns used for metrics
90-
if any(diffed_row[k] < 0 for k in metric_columns):
91-
# A "break" might be expected here instead of "continue," but there are cases where a subset of rows
92-
# are removed. To avoid situations where all results are discarded every check run, we err on the side
93-
# of potentially including truncated rows that exceed previous run counts.
73+
if metric_columns is None:
74+
metric_columns = metrics & row.keys() & prev.keys()
75+
if execution_indicators:
76+
indicator_cols = execution_indicators & metric_columns
77+
78+
# Check diffs before allocating an output dict: skip rows with
79+
# negative diffs (stats reset), zero change, or no execution indicator change.
80+
has_negative = False
81+
has_change = False
82+
for k in metric_columns:
83+
diff = row[k] - prev[k]
84+
if diff < 0:
85+
has_negative = True
86+
break
87+
if diff != 0:
88+
has_change = True
89+
90+
if has_negative or not has_change:
9491
continue
9592

96-
# If execution_indicators is specified, check if any of the execution indicator metrics have changed
97-
if execution_indicators:
98-
indicator_columns = execution_indicators & metric_columns
99-
if not any(diffed_row[k] > 0 for k in indicator_columns):
93+
if execution_indicators and indicator_cols:
94+
has_indicator_change = False
95+
for k in indicator_cols:
96+
if row[k] - prev[k] > 0:
97+
has_indicator_change = True
98+
break
99+
if not has_indicator_change:
100100
continue
101101

102-
# No changes to the query; no metric needed
103-
if all(diffed_row[k] == 0 for k in metric_columns):
104-
continue
102+
result.append({k: row[k] - prev[k] if k in metric_columns else row[k] for k in row})
105103

106-
result.append(diffed_row)
104+
# Update cache in-place: remove stale keys, update existing entries,
105+
# and only allocate new dicts for rows seen for the first time.
106+
new_keys = merged_rows.keys()
107+
stale_keys = self._previous_statements.keys() - new_keys
108+
for k in stale_keys:
109+
del self._previous_statements[k]
107110

108-
# Only cache the metric columns needed for derivative computation.
109-
# Non-metric columns (query text, metadata, etc.) are not needed for the diff calculation
110-
# and would waste memory. The returned diffed_row already uses current row values for
111-
# non-metric columns.
112-
self._previous_statements = {
113-
row_key: {col: row[col] for col in metrics if col in row} for row_key, row in merged_rows.items()
114-
}
111+
for row_key, row in merged_rows.items():
112+
prev = self._previous_statements.get(row_key)
113+
if prev is not None:
114+
# Sync columns to handle schema changes between calls.
115+
for col in metrics:
116+
if col in row:
117+
prev[col] = row[col]
118+
elif col in prev:
119+
del prev[col]
120+
else:
121+
self._previous_statements[row_key] = {col: row[col] for col in metrics if col in row}
115122

116123
return result
117124

0 commit comments

Comments
 (0)