From 807f398d81a2f30c182c4557773521549918bcce Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 25 Aug 2026 15:46:39 +0300 Subject: [PATCH] First repo code --- Cargo.lock | 25 ++-- Cargo.toml | 5 +- benches/repo_cache_bench.rs | 285 ++++++++++++++++++++++++++++++++++++ src/data.rs | 1 + src/data/entities.rs | 50 +++++++ src/data/repositories.rs | 46 ++++++ 6 files changed, 399 insertions(+), 13 deletions(-) create mode 100644 benches/repo_cache_bench.rs create mode 100644 src/data/repositories.rs diff --git a/Cargo.lock b/Cargo.lock index f86de79..6a66117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1066,7 +1066,7 @@ dependencies = [ "cosmic-text", "etagere", "lru", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "wgpu", ] @@ -2074,7 +2074,7 @@ dependencies = [ "lilt", "log", "num-traits", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "smol_str", "thiserror 2.0.18", "web-time", @@ -2100,7 +2100,7 @@ dependencies = [ "futures", "iced_core", "log", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "tokio", "wasm-bindgen-futures", "wasmtimer", @@ -2120,7 +2120,7 @@ dependencies = [ "iced_futures", "log", "raw-window-handle", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "thiserror 2.0.18", "unicode-segmentation", ] @@ -2174,7 +2174,7 @@ dependencies = [ "kurbo 0.10.4", "log", "resvg", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "softbuffer", "tiny-skia", ] @@ -2195,7 +2195,7 @@ dependencies = [ "iced_graphics", "log", "resvg", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "thiserror 2.0.18", "wgpu", ] @@ -2210,7 +2210,7 @@ dependencies = [ "log", "num-traits", "ouroboros", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "thiserror 2.0.18", "unicode-segmentation", ] @@ -2225,7 +2225,7 @@ dependencies = [ "iced_program", "log", "mundy", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "thiserror 2.0.18", "tracing", "wasm-bindgen-futures", @@ -2439,6 +2439,7 @@ dependencies = [ "rhai", "rodio", "rusqlite", + "rustc-hash 2.1.3", "serde_json", "sha2", "tokio", @@ -3835,7 +3836,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror 2.0.18", @@ -3857,7 +3858,7 @@ dependencies = [ "rand", "rand_pcg", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -4256,9 +4257,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" diff --git a/Cargo.toml b/Cargo.toml index 06ba459..ff04fc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,9 +37,12 @@ panic = "abort" [dev-dependencies] criterion = { version = "0.8.2", features = ["html_reports"] } +rustc-hash = "2.1.3" [[bench]] name = "map_bench" harness = false - +[[bench]] +name = "repo_cache_bench" +harness = false diff --git a/benches/repo_cache_bench.rs b/benches/repo_cache_bench.rs new file mode 100644 index 0000000..3d13a1a --- /dev/null +++ b/benches/repo_cache_bench.rs @@ -0,0 +1,285 @@ +use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId}; +use std::collections::{BTreeMap, HashMap}; +use std::hint::black_box; +use hashbrown::HashMap as HashbrownMap; +use rustc_hash::FxHashMap; + +// Тестовая структура (аналог вашей сущности) +#[derive(Debug, Clone)] +struct Entity { + id: i64, + data: [u8; 64], // 64 байта данных для реалистичности +} + +impl Entity { + fn new(id: i64) -> Self { + Self { + id, + data: [id as u8; 64], + } + } +} + +// ========== ТЕСТОВЫЕ ФУНКЦИИ ========== + +fn bench_vec_insert_sorted(vec: &mut Vec, entities: &[Entity]) { + vec.clear(); + for entity in entities { + vec.push(entity.clone()); + } + // Vec уже отсортирован, так как ID монотонно возрастают +} + +fn bench_vec_search_separated<'a>(ids: &[i64], vec: &'a [Entity], id: i64) -> Option<&'a Entity> { + match ids.binary_search(&id) { + Ok(index) => Some(&vec[index]), + Err(_) => None, + } +} + +fn bench_vec_search(vec: &Vec, id: i64) -> Option<&Entity> { + match vec.binary_search_by(|probe| probe.id.cmp(&id)) { + Ok(index) => Some(&vec[index]), + Err(_) => None, + } +} + +fn bench_btreemap_insert(map: &mut BTreeMap, entities: &[Entity]) { + map.clear(); + for entity in entities { + map.insert(entity.id, entity.clone()); + } +} + +fn bench_btreemap_search(map: &BTreeMap, id: i64) -> Option<&Entity> { + map.get(&id) +} + +fn bench_hashmap_insert(map: &mut HashMap, entities: &[Entity]) { + map.clear(); + for entity in entities { + map.insert(entity.id, entity.clone()); + } +} + +fn bench_hashmap_search(map: &HashMap, id: i64) -> Option<&Entity> { + map.get(&id) +} + +fn bench_hashbrown_insert(map: &mut HashbrownMap, entities: &[Entity]) { + map.clear(); + for entity in entities { + map.insert(entity.id, entity.clone()); + } +} + +fn bench_hashbrown_search(map: &HashbrownMap, id: i64) -> Option<&Entity> { + map.get(&id) +} + +fn bench_fxhashmap_insert(map: &mut FxHashMap, entities: &[Entity]) { + map.clear(); + for entity in entities { + map.insert(entity.id, entity.clone()); + } +} + +fn bench_fxhashmap_search(map: &FxHashMap, id: i64) -> Option<&Entity> { + map.get(&id) +} + +// ========== ГЕНЕРАЦИЯ ДАННЫХ ========== + +fn generate_entities(count: usize) -> Vec { + (0..count).map(|i| Entity::new(i as i64)).collect() +} + +fn generate_search_ids(count: usize, max_id: i64) -> Vec { + // 70% попаданий, 30% промахов (реалистичный сценарий) + let mut ids = Vec::with_capacity(count); + for i in 0..count { + if i % 10 < 7 { + // Попадание + ids.push((i as i64 * 13) % max_id); + } else { + // Промах (несуществующий ID) + ids.push(max_id + i as i64); + } + } + ids +} + +// ========== БЕНЧМАРКИ ========== + +fn bench_insert(c: &mut Criterion) { + let mut group = c.benchmark_group("insert"); + + for size in [10_000, 100_000, 500_000] { + let entities = generate_entities(size); + + group.bench_with_input(BenchmarkId::new("Vec (sorted)", size), &entities, |b, entities| { + let mut vec = Vec::with_capacity(size); + b.iter(|| { + bench_vec_insert_sorted(&mut vec, black_box(entities)); + }); + }); + + group.bench_with_input(BenchmarkId::new("BTreeMap", size), &entities, |b, entities| { + let mut map = BTreeMap::new(); + b.iter(|| { + bench_btreemap_insert(&mut map, black_box(entities)); + }); + }); + + group.bench_with_input(BenchmarkId::new("HashMap (std)", size), &entities, |b, entities| { + let mut map = HashMap::new(); + b.iter(|| { + bench_hashmap_insert(&mut map, black_box(entities)); + }); + }); + + group.bench_with_input(BenchmarkId::new("HashMap (hashbrown)", size), &entities, |b, entities| { + let mut map = HashbrownMap::new(); + b.iter(|| { + bench_hashbrown_insert(&mut map, black_box(entities)); + }); + }); + + group.bench_with_input(BenchmarkId::new("FxHashMap", size), &entities, |b, entities| { + let mut map = FxHashMap::default(); + b.iter(|| { + bench_fxhashmap_insert(&mut map, black_box(entities)); + }); + }); + } + + group.finish(); +} + +fn bench_search(c: &mut Criterion) { + let mut group = c.benchmark_group("search"); + + for size in [10_000, 100_000, 500_000] { + let entities = generate_entities(size); + let entity_ids = entities.iter().map(|entity| entity.id).collect::>(); + let search_ids = generate_search_ids(1000, size as i64); + + // Подготовка коллекций + let mut vec = Vec::with_capacity(size); + bench_vec_insert_sorted(&mut vec, &entities); + + let mut btreemap = BTreeMap::new(); + bench_btreemap_insert(&mut btreemap, &entities); + + let mut hashmap = HashMap::new(); + bench_hashmap_insert(&mut hashmap, &entities); + + let mut hashbrown = HashbrownMap::new(); + bench_hashbrown_insert(&mut hashbrown, &entities); + + let mut fxhashmap = FxHashMap::default(); + bench_fxhashmap_insert(&mut fxhashmap, &entities); + + group.bench_with_input(BenchmarkId::new("Vec (binary search)", size), &search_ids, |b, ids| { + b.iter(|| { + for &id in ids { + black_box(bench_vec_search_separated(&entity_ids, &vec, black_box(id))); + } + }); + }); + + group.bench_with_input(BenchmarkId::new("Vec (binary search) separated", size), &search_ids, |b, ids| { + b.iter(|| { + for &id in ids { + black_box(bench_vec_search(&vec, black_box(id))); + } + }); + }); + + group.bench_with_input(BenchmarkId::new("BTreeMap", size), &search_ids, |b, ids| { + b.iter(|| { + for &id in ids { + black_box(bench_btreemap_search(&btreemap, black_box(id))); + } + }); + }); + + group.bench_with_input(BenchmarkId::new("HashMap (std)", size), &search_ids, |b, ids| { + b.iter(|| { + for &id in ids { + black_box(bench_hashmap_search(&hashmap, black_box(id))); + } + }); + }); + + group.bench_with_input(BenchmarkId::new("HashMap (hashbrown)", size), &search_ids, |b, ids| { + b.iter(|| { + for &id in ids { + black_box(bench_hashbrown_search(&hashbrown, black_box(id))); + } + }); + }); + + group.bench_with_input(BenchmarkId::new("FxHashMap", size), &search_ids, |b, ids| { + b.iter(|| { + for &id in ids { + black_box(bench_fxhashmap_search(&fxhashmap, black_box(id))); + } + }); + }); + } + + group.finish(); +} + +fn bench_memory_efficiency(c: &mut Criterion) { + let mut group = c.benchmark_group("memory_efficiency"); + + let size = 100_000; + let entities = generate_entities(size); + + group.bench_function("Vec memory", |b| { + b.iter(|| { + let mut vec = Vec::with_capacity(size); + bench_vec_insert_sorted(&mut vec, black_box(&entities)); + black_box(vec); + }); + }); + + group.bench_function("BTreeMap memory", |b| { + b.iter(|| { + let mut map = BTreeMap::new(); + bench_btreemap_insert(&mut map, black_box(&entities)); + black_box(map); + }); + }); + + group.bench_function("HashMap (std) memory", |b| { + b.iter(|| { + let mut map = HashMap::new(); + bench_hashmap_insert(&mut map, black_box(&entities)); + black_box(map); + }); + }); + + group.bench_function("HashMap (hashbrown) memory", |b| { + b.iter(|| { + let mut map = HashbrownMap::new(); + bench_hashbrown_insert(&mut map, black_box(&entities)); + black_box(map); + }); + }); + + group.bench_function("FxHashMap memory", |b| { + b.iter(|| { + let mut map = FxHashMap::default(); + bench_fxhashmap_insert(&mut map, black_box(&entities)); + black_box(map); + }); + }); + + group.finish(); +} + +criterion_group!(benches, bench_insert, bench_search, bench_memory_efficiency); +criterion_main!(benches); \ No newline at end of file diff --git a/src/data.rs b/src/data.rs index 4589b9b..6c0c113 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,4 +1,5 @@ pub mod entities; +pub mod repositories; use rusqlite::ToSql; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; diff --git a/src/data/entities.rs b/src/data/entities.rs index 7342947..401e5b3 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -1,11 +1,14 @@ use crate::app::AppState; use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, OrderMode, WordOpenMode}; use crate::data::{INVALID_ID, Id}; +use crate::notification::MaybeError; use chrono::{DateTime, Utc}; use log::info; use rayon::prelude::*; use rhai::{Engine, Scope}; use std::collections::HashMap; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; use std::time::Instant; #[derive(Clone, Debug)] @@ -164,3 +167,50 @@ impl CardStatistics { (self.score as f32 * multiplier).max(1.0) } } + +#[derive(Clone)] +pub struct Entity> { + pub(crate) repo: R, + pub(crate) _phantom: PhantomData, + pub(crate) inner: EntityInner, +} + +#[derive(Clone)] +pub(crate) struct EntityInner { + pub(crate) id: Id, + pub(crate) real: bool, +} + +impl> Entity { + pub fn load(&self) -> EntityAccess { + let clone = *self.clone(); + let repo = &self.repo; + let value = repo.get(self.inner.id).unwrap(); + EntityAccess { value, entity: clone, } + } +} + +pub struct EntityAccess> { + entity: Entity, + value: E, +} + +impl> Deref for EntityAccess { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl> DerefMut for EntityAccess { + fn deref_mut(&mut self) -> &mut E { + &mut self.value + } +} + +pub trait Repository { + fn get(&self, id: Id) -> Option; + fn create(&mut self, new: T) -> Entity>; + fn save(&mut self, entity: EntityInner) -> MaybeError; +} diff --git a/src/data/repositories.rs b/src/data/repositories.rs new file mode 100644 index 0000000..dad8999 --- /dev/null +++ b/src/data/repositories.rs @@ -0,0 +1,46 @@ +use crate::data::Id; +use crate::data::entities::{Entity, EntityInner, Repository, WordData}; +use crate::notification::MaybeError; +use parking_lot::{Mutex, RwLock}; +use rusqlite::Connection; +use std::marker::PhantomData; +use std::sync::Arc; + +#[derive(Clone)] +pub struct WordRepository { + inner: Arc>, +} + +pub struct WordRepositoryInner { + cache: Vec, + temporal: Vec>, + next_temporal: u32, + connection: Arc>, +} + +impl Repository for WordRepository { + fn get(&self, id: Id) -> Option { + let inner = self.inner.read(); + Some(inner.cache[inner.cache.binary_search_by(|w| w.id.cmp(&id)).ok()?].clone()) + } + + fn create(&mut self, new: WordData) -> Entity { + let mut inner = self.inner.write(); + let next_temporal = inner.next_temporal; + inner.next_temporal += 1; + drop(inner); + let entity = Entity { + inner: EntityInner { + id: next_temporal.into(), + real: false, + }, + repo: self.clone(), + _phantom: PhantomData, + }; + entity + } + + fn save(&mut self, entity: EntityInner) -> MaybeError { + todo!() + } +}