feat(web): use reference counting rather than clones

before, cache would clone the underlying object every time it was
accessed. this was definitely incredibly wasteful! now it clones the
Arc, which is super cheap. the extra work necessary while fetching
things is most likely more than compensated by saved clones every time
we render stuff. there is some dumb arc creation here and there (like
for the debug page), but it's mostly my lazyness, they can probably go
and should not be that much of a big deal anyway.
TLDR things were already fast but should be EVEN FASTER!!!
This commit is contained in:
əlemi 2024-05-01 16:06:46 +02:00
parent e8b36cecdf
commit 9fe3cb3bda
Signed by: alemi
GPG key ID: A4895B84D311642C
6 changed files with 34 additions and 25 deletions

View file

@ -1,3 +1,4 @@
use std::sync::Arc;
use leptos::*;
use crate::prelude::*;
@ -6,10 +7,10 @@ use apb::{target::Addressed, Activity, Actor, Base, Object};
#[component]
pub fn ActivityLine(activity: serde_json::Value) -> impl IntoView {
pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
let object_id = activity.object().id().unwrap_or_default();
let actor_id = activity.actor().id().unwrap_or_default();
let actor = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone()));
let actor = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone()).into());
let avatar = actor.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default();
let username = actor.preferred_username().unwrap_or_default().to_string();
let domain = actor.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string();

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use leptos::*;
use crate::{prelude::*, URL_SENSITIVE};
@ -77,11 +79,11 @@ pub fn Attachment(
#[component]
pub fn Object(object: serde_json::Value) -> impl IntoView {
pub fn Object(object: crate::Object) -> impl IntoView {
let oid = object.id().unwrap_or_default().to_string();
let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
let author_id = object.attributed_to().id().unwrap_or_default();
let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone()));
let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone()).into());
let sensitive = object.sensitive().unwrap_or_default();
let addressed = object.addressed();
let public = addressed.iter().any(|x| x.as_str() == apb::target::PUBLIC);

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeSet, pin::Pin};
use std::{collections::BTreeSet, pin::Pin, sync::Arc};
use apb::{Activity, Base, Object};
use leptos::*;
@ -78,13 +78,13 @@ pub fn TimelineRepliesRecursive(tl: Timeline, root: String) -> impl IntoView {
Some(apb::ObjectType::Activity(_)) => x.object().id().map(|o| o == root).unwrap_or(false),
_ => x.in_reply_to().id().map(|r| r == root).unwrap_or(false),
})
.collect::<Vec<serde_json::Value>>();
.collect::<Vec<crate::Object>>();
view! {
<For
each=root_values
key=|k| k.id().unwrap_or_default().to_string()
children=move |object: serde_json::Value| {
children=move |object: crate::Object| {
match object.object_type() {
Some(apb::ObjectType::Activity(apb::ActivityType::Create)) => {
let oid = object.object().id().unwrap_or_default().to_string();
@ -220,7 +220,7 @@ async fn process_activities(
actors_seen.insert(attributed_to);
}
if let Some(object_uri) = object.id() {
CACHE.put(object_uri.to_string(), object.clone());
CACHE.put(object_uri.to_string(), Arc::new(object.clone()));
} else {
tracing::warn!("embedded object without id: {object:?}");
}
@ -239,7 +239,7 @@ async fn process_activities(
out.push(activity_id.to_string());
CACHE.put(
activity_id.to_string(),
activity.clone().set_object(apb::Node::maybe_link(object_id))
Arc::new(activity.clone().set_object(apb::Node::maybe_link(object_id)))
);
} else if let Some(object_id) = activity.object().id() {
out.push(object_id);
@ -271,7 +271,7 @@ async fn process_activities(
async fn fetch_and_update(kind: FetchKind, id: String, auth: Signal<Option<String>>) {
match Http::fetch(&Uri::api(kind, &id, false), auth).await {
Ok(data) => CACHE.put(id, data),
Ok(data) => CACHE.put(id, Arc::new(data)),
Err(e) => console_warn(&format!("could not fetch '{id}': {e}")),
}
}

View file

@ -5,10 +5,10 @@ use apb::{Actor, Base, Object};
#[component]
pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
match object {
pub fn ActorBanner(object: crate::Object) -> impl IntoView {
match object.as_ref() {
serde_json::Value::String(id) => view! {
<div><b>?</b>" "<a class="clean hover" href={Uri::web(FetchKind::User, &id)}>{Uri::pretty(&id)}</a></div>
<div><b>?</b>" "<a class="clean hover" href={Uri::web(FetchKind::User, id)}>{Uri::pretty(id)}</a></div>
},
serde_json::Value::Object(_) => {
let uid = object.id().unwrap_or_default().to_string();

View file

@ -21,23 +21,25 @@ lazy_static::lazy_static! {
pub static ref CACHE: ObjectCache = ObjectCache::default();
}
pub type Object = Arc<serde_json::Value>;
#[derive(Debug, Clone, Default)]
pub struct ObjectCache(pub Arc<dashmap::DashMap<String, serde_json::Value>>);
pub struct ObjectCache(pub Arc<dashmap::DashMap<String, Object>>);
impl ObjectCache {
pub fn get(&self, k: &str) -> Option<serde_json::Value> {
pub fn get(&self, k: &str) -> Option<Object> {
self.0.get(k).map(|x| x.clone())
}
pub fn get_or(&self, k: &str, or: serde_json::Value) -> serde_json::Value {
pub fn get_or(&self, k: &str, or: Object) -> Object {
self.get(k).unwrap_or(or)
}
pub fn put(&self, k: String, v: serde_json::Value) {
pub fn put(&self, k: String, v: Object) {
self.0.insert(k, v);
}
pub async fn fetch(&self, k: &str, kind: FetchKind) -> reqwest::Result<serde_json::Value> {
pub async fn fetch(&self, k: &str, kind: FetchKind) -> reqwest::Result<Object> {
match self.get(k) {
Some(x) => Ok(x),
None => {
@ -45,7 +47,7 @@ impl ObjectCache {
.await?
.json::<serde_json::Value>()
.await?;
self.put(k.to_string(), obj);
self.put(k.to_string(), Arc::new(obj));
Ok(self.get(k).expect("not found in cache after insertion"))
}
}

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use apb::{Actor, Base, Collection, Object};
use leptos::*;
@ -51,6 +53,7 @@ pub fn UserPage(tl: Timeline) -> impl IntoView {
Some(x) => Some(x.clone()),
None => {
let user : serde_json::Value = Http::fetch(&Uri::api(FetchKind::User, &id, true), auth).await.ok()?;
let user = Arc::new(user);
CACHE.put(Uri::full(FetchKind::User, &id), user.clone());
Some(user)
},
@ -163,6 +166,7 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
Some(x) => Some(x.clone()),
None => {
let obj = Http::fetch::<serde_json::Value>(&Uri::api(FetchKind::Object, &oid, true), auth).await.ok()?;
let obj = Arc::new(obj);
CACHE.put(Uri::full(FetchKind::Object, &oid), obj.clone());
Some(obj)
}
@ -225,9 +229,9 @@ pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView {
#[component]
pub fn DebugPage() -> impl IntoView {
let (object, set_object) = create_signal(serde_json::Value::String(
let (object, set_object) = create_signal(Arc::new(serde_json::Value::String(
"use this view to fetch remote AP objects and inspect their content".into())
);
));
let cached_ref: NodeRef<html::Input> = create_node_ref();
let auth = use_context::<Auth>().expect("missing auth context");
let (query, set_query) = create_signal("".to_string());
@ -242,14 +246,14 @@ pub fn DebugPage() -> impl IntoView {
if cached {
match CACHE.get(&fetch_url) {
Some(x) => set_object.set(x),
None => set_object.set(serde_json::Value::String("not in cache!".into())),
None => set_object.set(Arc::new(serde_json::Value::String("not in cache!".into()))),
}
} else {
let url = format!("{URL_BASE}/dbg?id={fetch_url}");
spawn_local(async move {
match Http::fetch::<serde_json::Value>(&url, auth).await {
Ok(x) => set_object.set(x),
Err(e) => set_object.set(serde_json::Value::String(e.to_string())),
Ok(x) => set_object.set(Arc::new(x)),
Err(e) => set_object.set(Arc::new(serde_json::Value::String(e.to_string()))),
}
});
}
@ -273,7 +277,7 @@ pub fn DebugPage() -> impl IntoView {
</form>
</div>
<pre class="ma-1" >
{move || serde_json::to_string_pretty(&object.get()).unwrap_or("unserializable".to_string())}
{move || serde_json::to_string_pretty(object.get().as_ref()).unwrap_or("unserializable".to_string())}
</pre>
</div>
}