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 leptos::*;
use crate::prelude::*; use crate::prelude::*;
@ -6,10 +7,10 @@ use apb::{target::Addressed, Activity, Actor, Base, Object};
#[component] #[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 object_id = activity.object().id().unwrap_or_default();
let actor_id = activity.actor().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 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 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(); 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 leptos::*;
use crate::{prelude::*, URL_SENSITIVE}; use crate::{prelude::*, URL_SENSITIVE};
@ -77,11 +79,11 @@ pub fn Attachment(
#[component] #[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 oid = object.id().unwrap_or_default().to_string();
let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
let author_id = object.attributed_to().id().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 sensitive = object.sensitive().unwrap_or_default();
let addressed = object.addressed(); let addressed = object.addressed();
let public = addressed.iter().any(|x| x.as_str() == apb::target::PUBLIC); 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 apb::{Activity, Base, Object};
use leptos::*; 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), 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), _ => x.in_reply_to().id().map(|r| r == root).unwrap_or(false),
}) })
.collect::<Vec<serde_json::Value>>(); .collect::<Vec<crate::Object>>();
view! { view! {
<For <For
each=root_values each=root_values
key=|k| k.id().unwrap_or_default().to_string() key=|k| k.id().unwrap_or_default().to_string()
children=move |object: serde_json::Value| { children=move |object: crate::Object| {
match object.object_type() { match object.object_type() {
Some(apb::ObjectType::Activity(apb::ActivityType::Create)) => { Some(apb::ObjectType::Activity(apb::ActivityType::Create)) => {
let oid = object.object().id().unwrap_or_default().to_string(); let oid = object.object().id().unwrap_or_default().to_string();
@ -220,7 +220,7 @@ async fn process_activities(
actors_seen.insert(attributed_to); actors_seen.insert(attributed_to);
} }
if let Some(object_uri) = object.id() { 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 { } else {
tracing::warn!("embedded object without id: {object:?}"); tracing::warn!("embedded object without id: {object:?}");
} }
@ -239,7 +239,7 @@ async fn process_activities(
out.push(activity_id.to_string()); out.push(activity_id.to_string());
CACHE.put( CACHE.put(
activity_id.to_string(), 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() { } else if let Some(object_id) = activity.object().id() {
out.push(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>>) { async fn fetch_and_update(kind: FetchKind, id: String, auth: Signal<Option<String>>) {
match Http::fetch(&Uri::api(kind, &id, false), auth).await { 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}")), Err(e) => console_warn(&format!("could not fetch '{id}': {e}")),
} }
} }

View file

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