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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
use {
crate::{
sm::SessionId,
sqlite::{
Sqlite, SqliteError, SqliteTransactionManager, SqliteUserId,
sqlite_api::{SqliteDb, SqliteStep, SqliteStmt},
},
utils::{errorfmt::ErrorFmt, thread_local_data::ThreadLocalData},
},
arrayvec::ArrayVec,
std::{cell::RefCell, rc::Rc, time::Instant},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum SessionOwnerError {
#[error(transparent)]
Sqlite(#[from] SqliteError),
#[error("The session is not owned by the current Jay instance")]
NotOwned,
}
pub fn assert_session_owner(
stmt: &mut SqliteStmt<'static>,
id: SessionId,
user_id: SqliteUserId,
) -> Result<(), SessionOwnerError> {
let mut stmt = stmt.activate();
stmt.bind_i64(1, id.0)?;
stmt.bind_user_id(2, user_id)?;
stmt.step()?;
if stmt.get_i64(0)? == 0 {
return Err(SessionOwnerError::NotOwned);
}
stmt.exec()?;
Ok(())
}
macro_rules! assert_session_owner {
($s:expr, $id:expr, $user_id:expr) => {
crate::sm::sm_jobs::sm_common::assert_session_owner(&mut $s.s.session_owned, $id, $user_id)
};
}
pub struct SmDbStateHolder {
state: ThreadLocalData<RefCell<SmDbState>>,
}
pub struct SmDbState {
pub sessions_created: u64,
pub toplevels_created: u64,
pub s: SmDbStatements,
}
macro_rules! statements {
($($name:ident,)*) => {
pub struct SmDbStatements {
$(pub $name: SqliteStmt<'static>,)*
}
impl SmDbStatements {
fn load(db: &Rc<SqliteDb>) -> Result<Self, SqliteError> {
Ok(Self {
$(
$name: db.prepare(include_str!(concat!("../sql/", stringify!($name), ".sql")))?,
)*
})
}
}
};
}
statements! {
session_del,
session_del_in_age_class,
session_del_unchecked,
session_disown,
session_list,
session_list_by_age_class,
session_load,
session_owned,
session_upsert,
toplevel_acquire,
toplevel_del,
toplevel_del_in_age_class,
toplevel_disown_all,
toplevel_disown_one,
toplevel_insert,
toplevel_list,
toplevel_list_by_age_class,
toplevel_rename,
toplevel_store,
}
impl SmDbStateHolder {
pub fn new(sqlite: &Sqlite) -> Self {
Self {
state: ThreadLocalData::new(sqlite.thread_id),
}
}
}
impl SmDbState {
fn new(db: &Rc<SqliteDb>, tx: &mut SqliteTransactionManager) -> Result<Self, SqliteError> {
db.exec(include_str!("../sql/init.sql"))?;
let mut slf = Self {
sessions_created: !0,
toplevels_created: !0,
s: SmDbStatements::load(db)?,
};
slf.run_gc(tx);
Ok(slf)
}
}
#[derive(Debug, Error)]
#[error("Could not create DB state")]
pub struct CreateDbStateError(#[source] SqliteError);
impl SmDbStateHolder {
pub fn get(
&self,
db: &Rc<SqliteDb>,
tx: &mut SqliteTransactionManager,
) -> Result<Rc<RefCell<SmDbState>>, CreateDbStateError> {
self.state.get_or_try_create(|| {
SmDbState::new(db, tx)
.map(RefCell::new)
.map(Rc::new)
.map_err(CreateDbStateError)
})
}
}
macro_rules! db_state {
($s:ident, $slf:expr, $ctx:expr) => {
let state = $slf.db_state.get(&$ctx.db, &mut $ctx.tx)?;
let $s = &mut *state.borrow_mut();
};
}
impl SmDbState {
pub fn run_gc(&mut self, tx: &mut SqliteTransactionManager) {
if let Err(e) = self.run_gc_(tx) {
log::error!("Could not run GC: {}", ErrorFmt(e));
}
}
fn run_gc_(&mut self, tx: &mut SqliteTransactionManager) -> Result<(), SqliteError> {
// We use the following garbage collection algorithm for sessions/toplevels in the
// database.
//
// First of all, objects that are currently being used by any compositior are
// never garbage collected. It is up to the compositor to limit the number of such
// objects in another way.
//
// All other rows are split into age classes. The age classes are determined as
// follows:
//
// floor(log2(max(16, atime - ctime))) - 4
//
// Where atime is the last time the row was accessed and ctime is the time when
// the row was created; both in seconds.
//
// This means that the age class is a function of the duration between first use
// and last use. An object created a year ago and accessed once within 3 seconds
// falls into the same age class as an object created 3 seconds ago.
//
// The function evaluates to 0 for everything under 32 seconds. After that, the
// upper bound of each age class is twice its lower bound. That is, the age
// classes are
//
// 0s - 32s
// 32s - 1m 4s
// 1m 4s - 2m 8s
// 2m 8s - 4m 16s
// 4m 16s - 8m 32s
// 8m 32s - 17m 4s
// 17m 4s - 34m 8s
// 34m 8s - 1h 8m 16s
// 1h 8m 16s - 2h 16m 32s
// 2h 16m 32s - 4h 33m 4s
//
// and so on.
//
// Each age class is garbage collected independently. This means that a flood of
// short-lived sessions will never cause long-lived sessions to be deleted.
//
// The maximum number of sessions within each age class is calculated as follows:
//
// 256 * pow(1.27, min(age_class, 26))
//
// where age_class is the index of the age class, starting at 0. For example,
//
// 0s - 32s: 256
// 32s - 1m 4s: 325
// 1m 4s - 2m 8s: 412
// 2m 8s - 4m 16s: 524
// 4m 16s - 8m 32s: 665
// 8m 32s - 17m 4s: 845
// 17m 4s - 34m 8s: 1074
// 34m 8s - 1h 8m 16s: 1364
// 1h 8m 16s - 2h 16m 32s: 1732
// 2h 16m 32s - 4h 33m 4s: 2200
// ...
// 34y 9d 1h 37m 4s - 68y 18d 3h 14m 8s: 127978
//
// If the number of rows in an age class exceeds this, we delete the row with the
// oldest atime until the number of rows no longer exceeds the maximum.
//
// The maximum number of toplevels is similarly calculated via
//
// 1024 * pow(1.3, min(age_class, 26))
//
// which yields
//
// 0s - 32s: 1024
// 32s - 1m 4s: 1331
// 1m 4s - 2m 8s: 1730
// 2m 8s - 4m 16s: 2249
// 4m 16s - 8m 32s: 2924
// 8m 32s - 17m 4s: 3802
// 17m 4s - 34m 8s: 4942
// 34m 8s - 1h 8m 16s: 6425
// 1h 8m 16s - 2h 16m 32s: 8353
// 2h 16m 32s - 4h 33m 4s: 10859
// ...
// 34y 9d 1h 37m 4s - 68y 18d 3h 14m 8s: 939349
//
// If we use 10 years as a reasonable upper bound for the lifetime of the database
// and sum the sizes of the age classes, we arrive at a maximum number of sessions
// of 372_263 and a maximum number of toplevels of 2_405_163. These values can be
// used to estimate the maximum size of the database.
const SESSION_MUL: f64 = 256.0;
const SESSION_BASE: f64 = 1.27;
const TOPLEVEL_MUL: f64 = 1024.0;
const TOPLEVEL_BASE: f64 = 1.3;
const MAX_AGE_CLASS: i64 = 26;
const SESSION_GC_INTERVAL: u64 = 256;
const TOPLEVEL_GC_INTERVAL: u64 = 1024;
let mut types = ArrayVec::<_, 2>::new();
if self.sessions_created >= SESSION_GC_INTERVAL {
self.sessions_created = 0;
types.push((
&mut self.s.session_list_by_age_class,
&mut self.s.session_del_in_age_class,
SESSION_MUL,
SESSION_BASE,
));
}
if self.toplevels_created >= TOPLEVEL_GC_INTERVAL {
self.toplevels_created = 0;
types.push((
&mut self.s.toplevel_list_by_age_class,
&mut self.s.toplevel_del_in_age_class,
TOPLEVEL_MUL,
TOPLEVEL_BASE,
));
}
if types.is_empty() {
return Ok(());
}
let start = Instant::now();
let tx = tx.begin_write()?;
for (list, del, mul, base) in types {
let mut stmt = list.activate();
while stmt.step()? == SqliteStep::Row {
let age_class = stmt.get_i64(0)?;
let count = stmt.get_i64(1)?;
let size_class = age_class.min(MAX_AGE_CLASS);
let size = (mul * base.powf(size_class as f64)) as i64;
if count > size {
let stmt = del.activate();
stmt.bind_i64(1, age_class)?;
stmt.bind_i64(2, count - size)?;
stmt.exec()?;
}
}
}
tx.commit()?;
log::info!("GC took {:?}", start.elapsed());
Ok(())
}
}