probabilistic_machines 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +58 -0
- data/Cargo.lock +294 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/ext/probabilistic_machines_native/core/Cargo.toml +24 -0
- data/ext/probabilistic_machines_native/core/src/countmin.rs +240 -0
- data/ext/probabilistic_machines_native/core/src/cuckoo.rs +283 -0
- data/ext/probabilistic_machines_native/core/src/hash.rs +88 -0
- data/ext/probabilistic_machines_native/core/src/hyperloglog.rs +254 -0
- data/ext/probabilistic_machines_native/core/src/lib.rs +65 -0
- data/ext/probabilistic_machines_native/core/src/tdigest.rs +445 -0
- data/ext/probabilistic_machines_native/core/src/topk.rs +325 -0
- data/ext/probabilistic_machines_native/extconf.rb +3 -0
- data/ext/probabilistic_machines_native/ffi/Cargo.toml +20 -0
- data/ext/probabilistic_machines_native/ffi/extconf.rb +21 -0
- data/ext/probabilistic_machines_native/ffi/src/countmin.rs +188 -0
- data/ext/probabilistic_machines_native/ffi/src/cuckoo.rs +149 -0
- data/ext/probabilistic_machines_native/ffi/src/data.rs +69 -0
- data/ext/probabilistic_machines_native/ffi/src/hyperloglog.rs +155 -0
- data/ext/probabilistic_machines_native/ffi/src/lib.rs +140 -0
- data/ext/probabilistic_machines_native/ffi/src/tdigest.rs +166 -0
- data/ext/probabilistic_machines_native/ffi/src/topk.rs +299 -0
- data/lib/probabilistic_machines/version.rb +5 -0
- data/lib/probabilistic_machines.rb +7 -0
- data/sig/probabilistic_machines.rbs +182 -0
- metadata +90 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
//! Which keys dominate a stream, in `K` counters instead of one per key.
|
|
2
|
+
|
|
3
|
+
use crate::hash::usize_low_u16;
|
|
4
|
+
use crate::{CodecError, TAG_TOPK, read8};
|
|
5
|
+
|
|
6
|
+
const HEADER_LEN: usize = 13;
|
|
7
|
+
const RECORD_LEN: usize = 24;
|
|
8
|
+
/// Where a record's count and overestimate begin, after its key.
|
|
9
|
+
const COUNT_AT: usize = 8;
|
|
10
|
+
const OVERESTIMATE_AT: usize = 16;
|
|
11
|
+
|
|
12
|
+
/// A tracked key, its counter, and how much of that counter it may have
|
|
13
|
+
/// inherited from keys evicted before it or from a merged-in stream that
|
|
14
|
+
/// tracked it no longer.
|
|
15
|
+
///
|
|
16
|
+
/// The true count lies in `count - overestimate ..= count`.
|
|
17
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
18
|
+
pub struct Hitter<T> {
|
|
19
|
+
pub key: T,
|
|
20
|
+
pub count: u64,
|
|
21
|
+
pub overestimate: u64,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Space-Saving over `K` counters: whatever is frequent ends up tracked.
|
|
25
|
+
///
|
|
26
|
+
/// After `n` observations every key that occurred more than `n / K` times is in
|
|
27
|
+
/// the table, and no tracked key's counter is below its true count. Lookups and
|
|
28
|
+
/// insertions scan all `K` counters, which is the trade for needing no heap and
|
|
29
|
+
/// no key ordering: keep `K` in the dozens-to-hundreds range.
|
|
30
|
+
///
|
|
31
|
+
/// ```
|
|
32
|
+
/// use probabilistic_machines::HeavyHitters;
|
|
33
|
+
///
|
|
34
|
+
/// let mut traffic = HeavyHitters::<u32, 8>::new();
|
|
35
|
+
/// for request in 0..1_000_u32 {
|
|
36
|
+
/// traffic.add(request % 97);
|
|
37
|
+
/// }
|
|
38
|
+
/// for _ in 0..5_000 {
|
|
39
|
+
/// traffic.add(7);
|
|
40
|
+
/// }
|
|
41
|
+
///
|
|
42
|
+
/// assert_eq!(traffic.observed(), 6_000);
|
|
43
|
+
/// assert!(traffic.count(&7) >= 5_000);
|
|
44
|
+
/// ```
|
|
45
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
46
|
+
pub struct HeavyHitters<T, const K: usize> {
|
|
47
|
+
slots: [Option<Hitter<T>>; K],
|
|
48
|
+
observed: u64,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl<T: Copy + Eq, const K: usize> HeavyHitters<T, K> {
|
|
52
|
+
#[must_use]
|
|
53
|
+
pub const fn new() -> Self {
|
|
54
|
+
Self {
|
|
55
|
+
slots: [const { None }; K],
|
|
56
|
+
observed: 0,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Observations fed in, saturating at [`u64::MAX`].
|
|
61
|
+
#[must_use]
|
|
62
|
+
pub const fn observed(&self) -> u64 {
|
|
63
|
+
self.observed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Keys currently tracked, at most `K`.
|
|
67
|
+
#[must_use]
|
|
68
|
+
pub fn len(&self) -> usize {
|
|
69
|
+
self.slots.iter().flatten().count()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[must_use]
|
|
73
|
+
pub fn is_empty(&self) -> bool {
|
|
74
|
+
self.slots.iter().all(Option::is_none)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Every tracked key, in no particular order.
|
|
78
|
+
pub fn hitters(&self) -> impl Iterator<Item = &Hitter<T>> {
|
|
79
|
+
self.slots.iter().flatten()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Upper bound on how often `key` occurred, 0 when it is not tracked.
|
|
83
|
+
#[must_use]
|
|
84
|
+
pub fn count(&self, key: &T) -> u64 {
|
|
85
|
+
self.hitters()
|
|
86
|
+
.find(|hitter| hitter.key == *key)
|
|
87
|
+
.map_or(0, |hitter| hitter.count)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The most frequent tracked key.
|
|
91
|
+
#[must_use]
|
|
92
|
+
pub fn top(&self) -> Option<&Hitter<T>> {
|
|
93
|
+
self.hitters().max_by_key(|hitter| hitter.count)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pub fn add(&mut self, key: T) {
|
|
97
|
+
self.add_count(key, 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pub fn add_count(&mut self, key: T, count: u64) {
|
|
101
|
+
self.observed = self.observed.saturating_add(count);
|
|
102
|
+
self.absorb(key, count, 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Folds another table in. Counts of keys tracked by both are exact sums.
|
|
106
|
+
/// A key only one side tracked may have occurred in the other stream too,
|
|
107
|
+
/// up to that side's eviction floor, so it takes the floor on both its
|
|
108
|
+
/// bounds: the counter still covers the true count and the interval
|
|
109
|
+
/// `count - overestimate ..= count` still holds it.
|
|
110
|
+
pub fn merge(&mut self, other: &Self) {
|
|
111
|
+
let floor = other.floor();
|
|
112
|
+
for tracked in self.slots.iter_mut().flatten() {
|
|
113
|
+
if other.hitters().all(|hitter| hitter.key != tracked.key) {
|
|
114
|
+
tracked.count = tracked.count.saturating_add(floor);
|
|
115
|
+
tracked.overestimate = tracked.overestimate.saturating_add(floor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for hitter in other.hitters() {
|
|
119
|
+
self.absorb(hitter.key, hitter.count, hitter.overestimate);
|
|
120
|
+
}
|
|
121
|
+
self.observed = self.observed.saturating_add(other.observed);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// How often an untracked key may have occurred: nothing while a counter is
|
|
125
|
+
/// still free, otherwise the weakest counter.
|
|
126
|
+
fn floor(&self) -> u64 {
|
|
127
|
+
if self.slots.iter().any(Option::is_none) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
self.hitters().map(|hitter| hitter.count).min().unwrap_or(0)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn absorb(&mut self, key: T, count: u64, overestimate: u64) {
|
|
134
|
+
if let Some(tracked) = self
|
|
135
|
+
.slots
|
|
136
|
+
.iter_mut()
|
|
137
|
+
.flatten()
|
|
138
|
+
.find(|hitter| hitter.key == key)
|
|
139
|
+
{
|
|
140
|
+
tracked.count = tracked.count.saturating_add(count);
|
|
141
|
+
tracked.overestimate = tracked.overestimate.saturating_add(overestimate);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if let Some(free) = self.slots.iter_mut().find(|slot| slot.is_none()) {
|
|
146
|
+
*free = Some(Hitter {
|
|
147
|
+
key,
|
|
148
|
+
count,
|
|
149
|
+
overestimate,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let Some(weakest) = self
|
|
155
|
+
.slots
|
|
156
|
+
.iter_mut()
|
|
157
|
+
.flatten()
|
|
158
|
+
.min_by_key(|hitter| hitter.count)
|
|
159
|
+
else {
|
|
160
|
+
return;
|
|
161
|
+
};
|
|
162
|
+
let floor = weakest.count;
|
|
163
|
+
*weakest = Hitter {
|
|
164
|
+
key,
|
|
165
|
+
count: floor.saturating_add(count),
|
|
166
|
+
overestimate: floor.saturating_add(overestimate),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
impl<const K: usize> HeavyHitters<u64, K> {
|
|
172
|
+
/// `K` as the frame's shape field.
|
|
173
|
+
const COUNTERS: [u8; 2] = {
|
|
174
|
+
assert!(K >= 1, "a table needs at least one counter");
|
|
175
|
+
assert!(K < 1 << 16, "a table needs fewer than 65536 counters");
|
|
176
|
+
usize_low_u16(K).to_le_bytes()
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/// Length of the [`encode`](Self::encode) frame.
|
|
180
|
+
pub const ENCODED_LEN: usize = HEADER_LEN + K * RECORD_LEN;
|
|
181
|
+
|
|
182
|
+
/// Writes the table into `out`, returning the number of bytes written.
|
|
183
|
+
///
|
|
184
|
+
/// The frame is the tag, `K`, the observation count, the number of tracked
|
|
185
|
+
/// keys and `K` records of key, count and overestimate. Records past the
|
|
186
|
+
/// tracked count are zero.
|
|
187
|
+
///
|
|
188
|
+
/// # Errors
|
|
189
|
+
///
|
|
190
|
+
/// [`CodecError::Truncated`] when `out` is shorter than
|
|
191
|
+
/// [`ENCODED_LEN`](Self::ENCODED_LEN).
|
|
192
|
+
pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
|
|
193
|
+
let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
|
|
194
|
+
return Err(CodecError::Truncated);
|
|
195
|
+
};
|
|
196
|
+
self.write_frame(frame);
|
|
197
|
+
Ok(Self::ENCODED_LEN)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// The table's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
201
|
+
#[cfg(feature = "alloc")]
|
|
202
|
+
#[must_use]
|
|
203
|
+
pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
|
|
204
|
+
let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
|
|
205
|
+
self.write_frame(&mut frame);
|
|
206
|
+
frame
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
210
|
+
fn write_frame(&self, frame: &mut [u8]) {
|
|
211
|
+
frame[0] = TAG_TOPK;
|
|
212
|
+
frame[1..3].copy_from_slice(&Self::COUNTERS);
|
|
213
|
+
frame[3..11].copy_from_slice(&self.observed.to_le_bytes());
|
|
214
|
+
frame[11..13].copy_from_slice(&usize_low_u16(self.len()).to_le_bytes());
|
|
215
|
+
frame[HEADER_LEN..].fill(0);
|
|
216
|
+
|
|
217
|
+
let (slots, _) = frame[HEADER_LEN..].as_chunks_mut::<RECORD_LEN>();
|
|
218
|
+
for (slot, hitter) in slots.iter_mut().zip(self.hitters()) {
|
|
219
|
+
slot[..COUNT_AT].copy_from_slice(&hitter.key.to_le_bytes());
|
|
220
|
+
slot[COUNT_AT..OVERESTIMATE_AT].copy_from_slice(&hitter.count.to_le_bytes());
|
|
221
|
+
slot[OVERESTIMATE_AT..].copy_from_slice(&hitter.overestimate.to_le_bytes());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Reads a table written by [`encode`](Self::encode).
|
|
226
|
+
///
|
|
227
|
+
/// # Errors
|
|
228
|
+
///
|
|
229
|
+
/// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
|
|
230
|
+
/// when it holds another structure, [`CodecError::Parameters`] when it was
|
|
231
|
+
/// written with a different counter count, claims more tracked keys than
|
|
232
|
+
/// `K`, or holds records no stream could have left.
|
|
233
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
|
|
234
|
+
let mut table = Self::new();
|
|
235
|
+
table.decode_into(bytes)?;
|
|
236
|
+
Ok(table)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Reads a frame over this table, for a caller that owns one already and
|
|
240
|
+
/// has no room on its stack for a second.
|
|
241
|
+
///
|
|
242
|
+
/// The table is untouched when the frame is rejected.
|
|
243
|
+
///
|
|
244
|
+
/// # Errors
|
|
245
|
+
///
|
|
246
|
+
/// The same as [`decode`](Self::decode).
|
|
247
|
+
pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
|
|
248
|
+
let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
|
|
249
|
+
return Err(CodecError::Truncated);
|
|
250
|
+
};
|
|
251
|
+
if frame[0] != TAG_TOPK {
|
|
252
|
+
return Err(CodecError::Tag);
|
|
253
|
+
}
|
|
254
|
+
if frame[1..3] != Self::COUNTERS {
|
|
255
|
+
return Err(CodecError::Parameters);
|
|
256
|
+
}
|
|
257
|
+
let tracked = usize::from(u16::from_le_bytes([frame[11], frame[12]]));
|
|
258
|
+
if tracked > K {
|
|
259
|
+
return Err(CodecError::Parameters);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let observed = u64::from_le_bytes(read8(&frame[3..11]));
|
|
263
|
+
let (slots, _) = frame[HEADER_LEN..].as_chunks::<RECORD_LEN>();
|
|
264
|
+
let records = &slots[..tracked];
|
|
265
|
+
if !records_are_coherent(records, observed) {
|
|
266
|
+
return Err(CodecError::Parameters);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
self.observed = observed;
|
|
270
|
+
for (slot, record) in self.slots.iter_mut().zip(records) {
|
|
271
|
+
*slot = Some(hitter(record));
|
|
272
|
+
}
|
|
273
|
+
self.slots[tracked..].fill(None);
|
|
274
|
+
Ok(())
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// A record as the hitter it stands for.
|
|
279
|
+
fn hitter(record: &[u8; RECORD_LEN]) -> Hitter<u64> {
|
|
280
|
+
Hitter {
|
|
281
|
+
key: u64::from_le_bytes(read8(&record[..COUNT_AT])),
|
|
282
|
+
count: u64::from_le_bytes(read8(&record[COUNT_AT..OVERESTIMATE_AT])),
|
|
283
|
+
overestimate: u64::from_le_bytes(read8(&record[OVERESTIMATE_AT..])),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Whether the records are ones Space-Saving could have tracked: a counter
|
|
288
|
+
/// stands for at least `count - overestimate` occurrences, which cannot outrun
|
|
289
|
+
/// the observations they came from, an interval runs upwards, and a key holds
|
|
290
|
+
/// one counter.
|
|
291
|
+
///
|
|
292
|
+
/// The counter itself may pass the observation count: a merge lifts a key the
|
|
293
|
+
/// other table evicted to that table's eviction floor and widens the
|
|
294
|
+
/// overestimate with it, so the two tables' floors can stack above the two
|
|
295
|
+
/// streams' length while the occurrences the interval claims stay inside it.
|
|
296
|
+
fn records_are_coherent(records: &[[u8; RECORD_LEN]], observed: u64) -> bool {
|
|
297
|
+
records.iter().enumerate().all(|(index, record)| {
|
|
298
|
+
let tracked = hitter(record);
|
|
299
|
+
tracked.overestimate <= tracked.count
|
|
300
|
+
&& tracked.count - tracked.overestimate <= observed
|
|
301
|
+
&& records[..index]
|
|
302
|
+
.iter()
|
|
303
|
+
.all(|earlier| hitter(earlier).key != tracked.key)
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
impl<T: Copy + Eq, const K: usize> Default for HeavyHitters<T, K> {
|
|
308
|
+
fn default() -> Self {
|
|
309
|
+
Self::new()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[cfg(feature = "alloc")]
|
|
314
|
+
impl<T: Copy + Eq, const K: usize> HeavyHitters<T, K> {
|
|
315
|
+
/// Tracked keys, most frequent first.
|
|
316
|
+
#[must_use]
|
|
317
|
+
pub fn ranked(&self) -> alloc::vec::Vec<Hitter<T>> {
|
|
318
|
+
let mut ranked: alloc::vec::Vec<Hitter<T>> = self.hitters().copied().collect();
|
|
319
|
+
ranked.sort_unstable_by_key(|hitter| core::cmp::Reverse(hitter.count));
|
|
320
|
+
ranked
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[cfg(test)]
|
|
325
|
+
mod tests;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "probabilistic_machines_native"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
authors = ["Abdelkader Boudih <oss@seuros.com>"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
description = "Ruby bindings for the probabilistic-machines sketches"
|
|
9
|
+
publish = false
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
|
|
14
|
+
[lints]
|
|
15
|
+
workspace = true
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
probabilistic-machines = { workspace = true, features = ["std"] }
|
|
19
|
+
magnus = { version = ">= 0.9.3", default-features = false, features = ["bytes", "io"] }
|
|
20
|
+
bytes = "1"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mkmf'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'rb_sys/mkmf'
|
|
6
|
+
|
|
7
|
+
create_rust_makefile('probabilistic_machines_native/probabilistic_machines_native') do |r|
|
|
8
|
+
r.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
|
|
9
|
+
r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rake-compiler builds from tmp/, where the generated relative manifest path no
|
|
13
|
+
# longer resolves.
|
|
14
|
+
makefile = File.join(Dir.pwd, 'Makefile')
|
|
15
|
+
File.write(
|
|
16
|
+
makefile,
|
|
17
|
+
File.read(makefile).gsub(
|
|
18
|
+
/^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
|
|
19
|
+
"RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
|
|
20
|
+
)
|
|
21
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
//! `ProbabilisticMachines::CountMinSketch`: how often each item was seen.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
7
|
+
use magnus::typed_data::Obj;
|
|
8
|
+
use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
|
|
9
|
+
use probabilistic_machines::{CodecError, CountMinSketch};
|
|
10
|
+
|
|
11
|
+
use crate::{Whole, codec_error, mismatch, optional_kwarg, unsupported};
|
|
12
|
+
|
|
13
|
+
/// Row widths [`Sketch`] is monomorphised for.
|
|
14
|
+
///
|
|
15
|
+
/// Capped so the widest sketch is 128 KiB. The core returns by value, so a
|
|
16
|
+
/// wider shape could not be built on Ruby's 256 KiB fiber stack without an
|
|
17
|
+
/// in-place constructor, and that needs `unsafe`.
|
|
18
|
+
const WIDTHS: [usize; 2] = [1024, 4096];
|
|
19
|
+
/// Row counts [`Sketch`] is monomorphised for.
|
|
20
|
+
const DEPTHS: [usize; 2] = [4, 8];
|
|
21
|
+
|
|
22
|
+
#[derive(Clone)]
|
|
23
|
+
enum Sketch {
|
|
24
|
+
W1024D4(Box<CountMinSketch<1024, 4>>),
|
|
25
|
+
W1024D8(Box<CountMinSketch<1024, 8>>),
|
|
26
|
+
W4096D4(Box<CountMinSketch<4096, 4>>),
|
|
27
|
+
W4096D8(Box<CountMinSketch<4096, 8>>),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Runs `body` against whichever variant is held.
|
|
31
|
+
macro_rules! sketch {
|
|
32
|
+
($sketch:expr, |$inner:ident| $body:expr) => {
|
|
33
|
+
match $sketch {
|
|
34
|
+
Sketch::W1024D4($inner) => $body,
|
|
35
|
+
Sketch::W1024D8($inner) => $body,
|
|
36
|
+
Sketch::W4096D4($inner) => $body,
|
|
37
|
+
Sketch::W4096D8($inner) => $body,
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[magnus::wrap(
|
|
43
|
+
class = "ProbabilisticMachines::CountMinSketch",
|
|
44
|
+
free_immediately,
|
|
45
|
+
size
|
|
46
|
+
)]
|
|
47
|
+
struct Wrapper(RefCell<Sketch>);
|
|
48
|
+
|
|
49
|
+
const fn width_of<const W: usize, const D: usize>(_: &CountMinSketch<W, D>) -> usize {
|
|
50
|
+
W
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fn depth_of<const W: usize, const D: usize>(_: &CountMinSketch<W, D>) -> usize {
|
|
54
|
+
D
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Each shape is constructed behind its own call frame. Inlined, a debug build
|
|
58
|
+
/// sizes one frame for every arm of the match below at once.
|
|
59
|
+
#[inline(never)]
|
|
60
|
+
fn boxed_new<const W: usize, const D: usize>() -> Box<CountMinSketch<W, D>> {
|
|
61
|
+
Box::new(CountMinSketch::new())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[inline(never)]
|
|
65
|
+
fn boxed_decode<const W: usize, const D: usize>(
|
|
66
|
+
ruby: &Ruby,
|
|
67
|
+
frame: &[u8],
|
|
68
|
+
) -> Result<Box<CountMinSketch<W, D>>, Error> {
|
|
69
|
+
let mut sketch = boxed_new::<W, D>();
|
|
70
|
+
sketch
|
|
71
|
+
.decode_into(frame)
|
|
72
|
+
.map_err(|error| codec_error(ruby, error))?;
|
|
73
|
+
Ok(sketch)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl Wrapper {
|
|
77
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
78
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
79
|
+
let (Whole(width), Whole(depth)) = get_kwargs::<_, (Whole<usize>, Whole<usize>), (), ()>(
|
|
80
|
+
args.keywords,
|
|
81
|
+
&["width", "depth"],
|
|
82
|
+
&[],
|
|
83
|
+
)?
|
|
84
|
+
.required;
|
|
85
|
+
if !DEPTHS.contains(&depth) {
|
|
86
|
+
return Err(unsupported(ruby, "depth", depth, &DEPTHS));
|
|
87
|
+
}
|
|
88
|
+
let sketch = match (width, depth) {
|
|
89
|
+
(1024, 4) => Sketch::W1024D4(boxed_new::<1024, 4>()),
|
|
90
|
+
(1024, 8) => Sketch::W1024D8(boxed_new::<1024, 8>()),
|
|
91
|
+
(4096, 4) => Sketch::W4096D4(boxed_new::<4096, 4>()),
|
|
92
|
+
(4096, 8) => Sketch::W4096D8(boxed_new::<4096, 8>()),
|
|
93
|
+
_ => return Err(unsupported(ruby, "width", width, &WIDTHS)),
|
|
94
|
+
};
|
|
95
|
+
Ok(Self(RefCell::new(sketch)))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
99
|
+
let Some(shape) = blob.get(1..3) else {
|
|
100
|
+
return Err(codec_error(ruby, CodecError::Truncated));
|
|
101
|
+
};
|
|
102
|
+
let sketch = match (shape[0], shape[1]) {
|
|
103
|
+
(10, 4) => Sketch::W1024D4(boxed_decode::<1024, 4>(ruby, &blob)?),
|
|
104
|
+
(10, 8) => Sketch::W1024D8(boxed_decode::<1024, 8>(ruby, &blob)?),
|
|
105
|
+
(12, 4) => Sketch::W4096D4(boxed_decode::<4096, 4>(ruby, &blob)?),
|
|
106
|
+
(12, 8) => Sketch::W4096D8(boxed_decode::<4096, 8>(ruby, &blob)?),
|
|
107
|
+
_ => {
|
|
108
|
+
return Err(Error::new(
|
|
109
|
+
ruby.exception_arg_error(),
|
|
110
|
+
format!("blob was written with a shape outside {WIDTHS:?} x {DEPTHS:?}"),
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
Ok(Self(RefCell::new(sketch)))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn width(&self) -> usize {
|
|
118
|
+
sketch!(&*self.0.borrow(), |sketch| width_of(&**sketch))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn depth(&self) -> usize {
|
|
122
|
+
sketch!(&*self.0.borrow(), |sketch| depth_of(&**sketch))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn add(rb_self: &Self, args: &[Value]) -> Result<(), Error> {
|
|
126
|
+
let args = scan_args::<(Bytes,), (), (), (), RHash, ()>(args)?;
|
|
127
|
+
let (item,) = args.required;
|
|
128
|
+
let count =
|
|
129
|
+
optional_kwarg::<Whole<u32>>(args.keywords, "count")?.map_or(1, |Whole(count)| count);
|
|
130
|
+
sketch!(&mut *rb_self.0.borrow_mut(), |sketch| sketch
|
|
131
|
+
.add_count(&item, count));
|
|
132
|
+
Ok(())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn count(&self, item: Bytes) -> u32 {
|
|
136
|
+
sketch!(&*self.0.borrow(), |sketch| sketch.estimate(&item))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn saturated(&self) -> bool {
|
|
140
|
+
sketch!(&*self.0.borrow(), |sketch| sketch.saturated())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn total(&self) -> u64 {
|
|
144
|
+
sketch!(&*self.0.borrow(), |sketch| sketch.total())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
|
|
148
|
+
let source = other.0.borrow().clone();
|
|
149
|
+
match (&mut *rb_self.0.borrow_mut(), &source) {
|
|
150
|
+
(Sketch::W1024D4(target), Sketch::W1024D4(source)) => target.merge(source),
|
|
151
|
+
(Sketch::W1024D8(target), Sketch::W1024D8(source)) => target.merge(source),
|
|
152
|
+
(Sketch::W4096D4(target), Sketch::W4096D4(source)) => target.merge(source),
|
|
153
|
+
(Sketch::W4096D8(target), Sketch::W4096D8(source)) => target.merge(source),
|
|
154
|
+
_ => return Err(mismatch(ruby, "sketch")),
|
|
155
|
+
}
|
|
156
|
+
Ok(rb_self)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
160
|
+
let frame = sketch!(&*rb_self.0.borrow(), |sketch| sketch.encoded());
|
|
161
|
+
ruby.str_from_slice(&frame)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn inspect(&self) -> String {
|
|
165
|
+
format!(
|
|
166
|
+
"#<ProbabilisticMachines::CountMinSketch width={} depth={} total={}>",
|
|
167
|
+
self.width(),
|
|
168
|
+
self.depth(),
|
|
169
|
+
self.total()
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
175
|
+
let class = module.define_class("CountMinSketch", ruby.class_object())?;
|
|
176
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
177
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
178
|
+
class.define_method("width", method!(Wrapper::width, 0))?;
|
|
179
|
+
class.define_method("depth", method!(Wrapper::depth, 0))?;
|
|
180
|
+
class.define_method("add", method!(Wrapper::add, -1))?;
|
|
181
|
+
class.define_method("count", method!(Wrapper::count, 1))?;
|
|
182
|
+
class.define_method("total", method!(Wrapper::total, 0))?;
|
|
183
|
+
class.define_method("saturated?", method!(Wrapper::saturated, 0))?;
|
|
184
|
+
class.define_method("merge", method!(Wrapper::merge, 1))?;
|
|
185
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
186
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
187
|
+
Ok(())
|
|
188
|
+
}
|