xref: /freebsd/sys/contrib/openzfs/module/zfs/zfs_crrd.c (revision 22649d4dba730d46244fd2dff4fd174903c8379f)
1 // SPDX-License-Identifier: CDDL-1.0
2 /*
3  * This file and its contents are supplied under the terms of the
4  * Common Development and Distribution License ("CDDL"), version 1.0.
5  * You may only use this file in accordance with the terms of version
6  * 1.0 of the CDDL.
7  *
8  * A full copy of the text of the CDDL should have accompanied this
9  * source.  A copy of the CDDL is also available via the Internet at
10  * https://opensource.org/license/CDDL-1.0.
11  */
12 /*
13  * Copyright (c) 2024 Klara Inc.
14  *
15  * This software was developed by
16  * Mariusz Zaborski <mariusz.zaborski@klarasystems.com>
17  * Fred Weigel <fred.weigel@klarasystems.com>
18  * under sponsorship from Wasabi Technology, Inc. and Klara Inc.
19  */
20 /*
21  * This file implements a round-robin database that stores timestamps and txg
22  * numbers. Due to limited space, we use a round-robin approach, where
23  * the oldest records are overwritten when there is no longer enough room.
24  * This is a best-effort mechanism, and the database should be treated as
25  * an approximation. Consider this before consuming it.
26  *
27  * The database is linear, meaning we assume each new entry is newer than the
28  * ones already stored. Because of this, if time is manipulated, the database
29  * will only accept records that are newer than the existing ones.
30  * (For example, jumping 10 years into the future and then back can lead to
31  * situation when for 10 years we wont write anything to database)
32  *
33  * All times stored in the database use UTC, which makes it easy to convert to
34  * and from local time.
35  *
36  * Each database holds 256 records (as defined in the `RRD_MAX_ENTRIES` macro).
37  * This limit comes from the maximum size of a ZAP object, where we store the
38  * binary blob.
39  *
40  * We've split the database into three smaller ones.
41  * The `minute database` provides high resolution (default: every 10 minutes),
42  * but only covers approximately 1.5 days. This gives a detailed view of recent
43  * activity, useful, for example, when performing a scrub of the last hour.
44  * The `daily database` records one txg per day. With 256 entries, it retains
45  * roughly 8 months of data. This allows users to scrub or analyze txgs across
46  * a range of days.
47  * The `monthly database` stores one record per month, giving approximately
48  * 21 years of history.
49  * All these calculations assume the worst-case scenario: the pool is always
50  * online and actively written to.
51  *
52  * A potential source of confusion is that the database does not store data
53  * while the pool is offline, leading to potential gaps in timeline. Also,
54  * the database contains no records from before this feature was enabled.
55  * Both, upon reflection, are expected.
56  */
57 #include <sys/zfs_context.h>
58 
59 #include "zfs_crrd.h"
60 
61 rrd_data_t *
rrd_tail_entry(rrd_t * rrd)62 rrd_tail_entry(rrd_t *rrd)
63 {
64 	size_t n;
65 
66 	if (rrd_len(rrd) == 0)
67 		return (NULL);
68 
69 	if (rrd->rrd_tail == 0)
70 		n = RRD_MAX_ENTRIES - 1;
71 	else
72 		n = rrd->rrd_tail - 1;
73 
74 	return (&rrd->rrd_entries[n]);
75 }
76 
77 uint64_t
rrd_tail(rrd_t * rrd)78 rrd_tail(rrd_t *rrd)
79 {
80 	const rrd_data_t *tail;
81 
82 	tail = rrd_tail_entry(rrd);
83 
84 	return (tail == NULL ? 0 : tail->rrdd_time);
85 }
86 
87 /*
88  * Return length of data in the rrd.
89  * rrd_get works from 0..rrd_len()-1.
90  */
91 size_t
rrd_len(const rrd_t * rrd)92 rrd_len(const rrd_t *rrd)
93 {
94 
95 	return (rrd->rrd_length);
96 }
97 
98 const rrd_data_t *
rrd_entry(const rrd_t * rrd,size_t i)99 rrd_entry(const rrd_t *rrd, size_t i)
100 {
101 	size_t n;
102 
103 	if (i >= rrd_len(rrd)) {
104 		return (0);
105 	}
106 
107 	n = (rrd->rrd_head + i) % RRD_MAX_ENTRIES;
108 	return (&rrd->rrd_entries[n]);
109 }
110 
111 uint64_t
rrd_get(const rrd_t * rrd,size_t i)112 rrd_get(const rrd_t *rrd, size_t i)
113 {
114 	const rrd_data_t *data = rrd_entry(rrd, i);
115 
116 	return (data == NULL ? 0 : data->rrdd_txg);
117 }
118 
119 /* Add value to database. */
120 void
rrd_add(rrd_t * rrd,hrtime_t time,uint64_t txg)121 rrd_add(rrd_t *rrd, hrtime_t time, uint64_t txg)
122 {
123 	rrd_data_t *tail;
124 
125 	tail = rrd_tail_entry(rrd);
126 	if (tail != NULL && tail->rrdd_time == time) {
127 		if (tail->rrdd_txg < txg) {
128 			tail->rrdd_txg = txg;
129 		} else {
130 			return;
131 		}
132 	}
133 
134 	rrd->rrd_entries[rrd->rrd_tail].rrdd_time = time;
135 	rrd->rrd_entries[rrd->rrd_tail].rrdd_txg = txg;
136 
137 	rrd->rrd_tail = (rrd->rrd_tail + 1) % RRD_MAX_ENTRIES;
138 
139 	if (rrd->rrd_length < RRD_MAX_ENTRIES) {
140 		rrd->rrd_length++;
141 	} else {
142 		rrd->rrd_head = (rrd->rrd_head + 1) % RRD_MAX_ENTRIES;
143 	}
144 }
145 
146 void
dbrrd_add(dbrrd_t * db,hrtime_t time,uint64_t txg)147 dbrrd_add(dbrrd_t *db, hrtime_t time, uint64_t txg)
148 {
149 	hrtime_t daydiff, monthdiff, minutedif;
150 
151 	minutedif = time - rrd_tail(&db->dbr_minutes);
152 	daydiff = time - rrd_tail(&db->dbr_days);
153 	monthdiff = time - rrd_tail(&db->dbr_months);
154 
155 	if (monthdiff >= 0 && monthdiff >= 30 * 24 * 60 * 60)
156 		rrd_add(&db->dbr_months, time, txg);
157 	else if (daydiff >= 0 && daydiff >= 24 * 60 * 60)
158 		rrd_add(&db->dbr_days, time, txg);
159 	else if (minutedif >= 0)
160 		rrd_add(&db->dbr_minutes, time, txg);
161 }
162 
163 /*
164  * We could do a binary search here, but the routine isn't frequently
165  * called and the data is small so we stick to a simple loop.
166  */
167 static const rrd_data_t *
rrd_query(rrd_t * rrd,hrtime_t tv,dbrrd_rounding_t rounding)168 rrd_query(rrd_t *rrd, hrtime_t tv, dbrrd_rounding_t rounding)
169 {
170 	const rrd_data_t *data = NULL;
171 
172 	for (size_t i = 0; i < rrd_len(rrd); i++) {
173 		const rrd_data_t *cur = rrd_entry(rrd, i);
174 
175 		if (rounding == DBRRD_FLOOR) {
176 			if (tv < cur->rrdd_time) {
177 				break;
178 			}
179 			data = cur;
180 		} else {
181 			/* DBRRD_CEILING */
182 			if (tv <= cur->rrdd_time) {
183 				data = cur;
184 				break;
185 			}
186 		}
187 	}
188 
189 	return (data);
190 }
191 
192 static const rrd_data_t *
dbrrd_closest(hrtime_t tv,const rrd_data_t * r1,const rrd_data_t * r2)193 dbrrd_closest(hrtime_t tv, const rrd_data_t *r1, const rrd_data_t *r2)
194 {
195 
196 	if (r1 == NULL)
197 		return (r2);
198 	if (r2 == NULL)
199 		return (r1);
200 
201 	return (ABS(tv - (hrtime_t)r1->rrdd_time) <
202 	    ABS(tv - (hrtime_t)r2->rrdd_time) ? r1 : r2);
203 }
204 
205 uint64_t
dbrrd_query(dbrrd_t * r,hrtime_t tv,dbrrd_rounding_t rounding)206 dbrrd_query(dbrrd_t *r, hrtime_t tv, dbrrd_rounding_t rounding)
207 {
208 	const rrd_data_t *data, *dm, *dd, *dy;
209 
210 	data = NULL;
211 	dm = rrd_query(&r->dbr_minutes, tv, rounding);
212 	dd = rrd_query(&r->dbr_days, tv, rounding);
213 	dy = rrd_query(&r->dbr_months, tv, rounding);
214 
215 	data = dbrrd_closest(tv, dbrrd_closest(tv, dd, dm), dy);
216 
217 	return (data == NULL ? 0 : data->rrdd_txg);
218 }
219 
220 hrtime_t
dbrrd_latest_time(dbrrd_t * r)221 dbrrd_latest_time(dbrrd_t *r)
222 {
223 	const rrd_data_t *head;
224 	const rrd_t *curdb;
225 	size_t dblen;
226 
227 	curdb = &r->dbr_minutes;
228 	dblen = rrd_len(curdb);
229 	if (dblen == 0)
230 		return (0);
231 
232 	head = rrd_entry(curdb, dblen - 1);
233 	return (head->rrdd_time);
234 }
235