fix web performance
This commit is contained in:
Generated
+3
@@ -1445,6 +1445,7 @@ dependencies = [
|
|||||||
"gpui_platform",
|
"gpui_platform",
|
||||||
"gpui_web",
|
"gpui_web",
|
||||||
"instant",
|
"instant",
|
||||||
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"person",
|
"person",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -1455,6 +1456,7 @@ dependencies = [
|
|||||||
"universal-time 0.3.1 (git+https://github.com/shadowylab/universal-time)",
|
"universal-time 0.3.1 (git+https://github.com/shadowylab/universal-time)",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4982,6 +4984,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"instant",
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
+15
-11
@@ -290,34 +290,38 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
||||||
while let Ok(message) = rx.recv_async().await {
|
while let Ok(message) = rx.recv_async().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
// Drain the whole queue in a single update so a burst of
|
||||||
|
// events (e.g. history sync after login) collapses into
|
||||||
|
// one repaint instead of one per message (important on
|
||||||
|
// wasm, where everything runs on the main thread).
|
||||||
|
let mut batch = vec![message];
|
||||||
|
while let Ok(extra) = rx.try_recv() {
|
||||||
|
batch.push(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
for message in batch {
|
||||||
match message {
|
match message {
|
||||||
Signal::Message(message) => {
|
Signal::Message(message) => {
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.new_message(message, cx);
|
this.new_message(message, cx);
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Signal::InboxReady => {
|
Signal::InboxReady => {
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.get_messages(cx);
|
this.get_messages(cx);
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Signal::Eose => {
|
Signal::Eose => {
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
this.tracking.store(false, Ordering::Release);
|
this.tracking.store(false, Ordering::Release);
|
||||||
})?;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Signal::Error(failed) => {
|
Signal::Error(failed) => {
|
||||||
trash.update(cx, |this, cx| {
|
let _ = trash.update(cx, |this, cx| {
|
||||||
this.insert(failed);
|
this.insert(failed);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -242,6 +242,16 @@ impl ChatPanel {
|
|||||||
while let Ok(status) = rx.recv_async().await {
|
while let Ok(status) = rx.recv_async().await {
|
||||||
{
|
{
|
||||||
let mut map = reports.write().unwrap();
|
let mut map = reports.write().unwrap();
|
||||||
|
|
||||||
|
// Drain the whole queue in a single update so bursts of
|
||||||
|
// send statuses collapse into one repaint (important on
|
||||||
|
// wasm, where everything runs on the main thread).
|
||||||
|
let mut statuses = vec![status];
|
||||||
|
while let Ok(extra) = rx.try_recv() {
|
||||||
|
statuses.push(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
for status in statuses {
|
||||||
let status_id = match &*status {
|
let status_id = match &*status {
|
||||||
SendStatus::Ok { id, .. } => *id,
|
SendStatus::Ok { id, .. } => *id,
|
||||||
SendStatus::Failed { id, .. } => *id,
|
SendStatus::Failed { id, .. } => *id,
|
||||||
@@ -268,6 +278,7 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.update(cx, |_, cx| cx.notify()).ok();
|
this.update(cx, |_, cx| cx.notify()).ok();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ anyhow.workspace = true
|
|||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|||||||
+42
-16
@@ -3,7 +3,8 @@ use std::sync::RwLock;
|
|||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventExt;
|
use common::EventExt;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
use futures::FutureExt;
|
||||||
|
use gpui::{App, AppContext, BackgroundExecutor, Context, Entity, Global, Task, Window};
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -72,13 +73,24 @@ impl PersonRegistry {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let client3 = client.clone();
|
let client3 = client.clone();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
tasks.push(cx.background_spawn(async move {
|
tasks.push(cx.background_spawn(async move {
|
||||||
Self::handle_requests(&client3, &metadata_rx).await;
|
Self::handle_requests(&client3, &metadata_rx, &executor).await;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
tasks.push(cx.spawn(async move |this, cx| {
|
tasks.push(cx.spawn(async move |this, cx| {
|
||||||
while let Ok(event) = rx.recv_async().await {
|
while let Ok(event) = rx.recv_async().await {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
// Drain the whole queue in a single update so a burst of
|
||||||
|
// events collapses into one repaint instead of one per
|
||||||
|
// event (important on wasm, where everything runs on the
|
||||||
|
// main thread).
|
||||||
|
let mut dispatch = vec![event];
|
||||||
|
while let Ok(extra) = rx.try_recv() {
|
||||||
|
dispatch.push(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
for event in dispatch {
|
||||||
match event {
|
match event {
|
||||||
Dispatch::Person(person) => {
|
Dispatch::Person(person) => {
|
||||||
this.insert(person, cx);
|
this.insert(person, cx);
|
||||||
@@ -90,6 +102,7 @@ impl PersonRegistry {
|
|||||||
this.set_messaging_relays(&event, cx);
|
this.set_messaging_relays(&event, cx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -156,24 +169,39 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle request for metadata
|
/// Handle request for metadata
|
||||||
async fn handle_requests(client: &Client, rx: &flume::Receiver<PublicKey>) {
|
///
|
||||||
|
/// Requests are collected into batches and flushed when the batch is
|
||||||
|
/// full or the timeout expires.
|
||||||
|
///
|
||||||
|
/// Note: `flume::Selector::wait_timeout` is intentionally not used here:
|
||||||
|
/// it relies on `std::time::Instant` and `thread::park_timeout`, which are
|
||||||
|
/// unavailable on `wasm32-unknown-unknown` (the former panics, the latter
|
||||||
|
/// is a no-op that would turn the wait into a busy loop on the main
|
||||||
|
/// thread).
|
||||||
|
async fn handle_requests(
|
||||||
|
client: &Client,
|
||||||
|
rx: &flume::Receiver<PublicKey>,
|
||||||
|
executor: &BackgroundExecutor,
|
||||||
|
) {
|
||||||
let mut batch: HashSet<PublicKey> = HashSet::new();
|
let mut batch: HashSet<PublicKey> = HashSet::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match flume::Selector::new()
|
// Wait for the next request, or the batch timeout.
|
||||||
.recv(rx, |result| result.ok())
|
futures::select! {
|
||||||
.wait_timeout(Duration::from_secs(TIMEOUT))
|
result = rx.recv_async() => match result {
|
||||||
{
|
Ok(public_key) => {
|
||||||
Ok(Some(public_key)) => {
|
|
||||||
batch.insert(public_key);
|
batch.insert(public_key);
|
||||||
// Process the batch if it's full
|
// Keep collecting until the batch is full
|
||||||
if batch.len() >= 20
|
if batch.len() < 20 {
|
||||||
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
continue;
|
||||||
{
|
|
||||||
log::warn!("Failed to get metadata batch: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
Err(_) => return,
|
||||||
|
},
|
||||||
|
_ = executor.timer(Duration::from_secs(TIMEOUT)).fuse() => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the batch
|
||||||
if !batch.is_empty()
|
if !batch.is_empty()
|
||||||
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
||||||
{
|
{
|
||||||
@@ -181,8 +209,6 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load all user profiles from the database
|
/// Load all user profiles from the database
|
||||||
fn load(&mut self, cx: &mut Context<Self>) {
|
fn load(&mut self, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -105,7 +105,21 @@ impl Person {
|
|||||||
|
|
||||||
/// Get profile avatar
|
/// Get profile avatar
|
||||||
pub fn avatar(&self) -> SharedString {
|
pub fn avatar(&self) -> SharedString {
|
||||||
self.metadata()
|
// On web, the browser blocks cross-origin image fetches unless the
|
||||||
|
// picture host sends CORS headers, so remote avatars can never load
|
||||||
|
// there. Each doomed fetch still triggers a full-window repaint when
|
||||||
|
// it fails, which is very costly on wasm's single main thread when a
|
||||||
|
// list renders many avatars at once. Fall back to the bundled avatar
|
||||||
|
// for remote pictures on web.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
if let Some(picture) = self.metadata.picture.as_ref()
|
||||||
|
&& !picture.is_empty()
|
||||||
|
&& url::Url::parse(picture).is_ok_and(|url| matches!(url.scheme(), "http" | "https"))
|
||||||
|
{
|
||||||
|
return "brand/avatar.png".into();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.metadata
|
||||||
.picture
|
.picture
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|picture| !picture.is_empty())
|
.filter(|picture| !picture.is_empty())
|
||||||
@@ -115,13 +129,13 @@ impl Person {
|
|||||||
|
|
||||||
/// Get profile name
|
/// Get profile name
|
||||||
pub fn name(&self) -> SharedString {
|
pub fn name(&self) -> SharedString {
|
||||||
if let Some(display_name) = self.metadata().display_name.as_ref()
|
if let Some(display_name) = self.metadata.display_name.as_ref()
|
||||||
&& !display_name.is_empty()
|
&& !display_name.is_empty()
|
||||||
{
|
{
|
||||||
return SharedString::from(display_name.trim());
|
return SharedString::from(display_name.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = self.metadata().name.as_ref()
|
if let Some(name) = self.metadata.name.as_ref()
|
||||||
&& !name.is_empty()
|
&& !name.is_empty()
|
||||||
{
|
{
|
||||||
return SharedString::from(name.trim());
|
return SharedString::from(name.trim());
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ tracing-wasm = "0.2"
|
|||||||
console_log = "1.0"
|
console_log = "1.0"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
js-sys = "0.3"
|
||||||
|
web-sys = { version = "0.3", features = ["Window", "Performance"] }
|
||||||
universal-time = { git = "https://github.com/shadowylab/universal-time" }
|
universal-time = { git = "https://github.com/shadowylab/universal-time" }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
|||||||
+10
-2
@@ -11,13 +11,21 @@ struct CustomTimeProvider;
|
|||||||
|
|
||||||
impl WallClock for CustomTimeProvider {
|
impl WallClock for CustomTimeProvider {
|
||||||
fn system_time(&self) -> SystemTime {
|
fn system_time(&self) -> SystemTime {
|
||||||
SystemTime::from_unix_duration(instant::Duration::from_secs(0))
|
// Browser wall clock: milliseconds since the Unix epoch.
|
||||||
|
let millis = js_sys::Date::now();
|
||||||
|
SystemTime::from_unix_duration(instant::Duration::from_millis(millis as u64))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MonotonicClock for CustomTimeProvider {
|
impl MonotonicClock for CustomTimeProvider {
|
||||||
fn instant(&self) -> Instant {
|
fn instant(&self) -> Instant {
|
||||||
Instant::from_ticks(instant::Duration::from_secs(0))
|
// `performance.now()` is monotonic; fall back to the wall clock if
|
||||||
|
// it's unavailable.
|
||||||
|
let millis = web_sys::window()
|
||||||
|
.and_then(|window| window.performance())
|
||||||
|
.map(|performance| performance.now())
|
||||||
|
.unwrap_or_else(js_sys::Date::now);
|
||||||
|
Instant::from_ticks(instant::Duration::from_millis(millis as u64))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export default defineConfig({
|
|||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
src: path.resolve(__dirname, "../../../assets/icons"),
|
src: path.resolve(__dirname, "../../assets/icons"),
|
||||||
dest: "assets",
|
dest: "assets",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: path.resolve(__dirname, "../../../assets/brand"),
|
src: path.resolve(__dirname, "../../assets/brand"),
|
||||||
dest: "assets",
|
dest: "assets",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
server.middlewares.use(
|
server.middlewares.use(
|
||||||
"/assets",
|
"/assets",
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
const assetsPath = path.resolve(__dirname, "../../../assets");
|
const assetsPath = path.resolve(__dirname, "../../assets");
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
assetsPath,
|
assetsPath,
|
||||||
req.url.replace("/assets", ""),
|
req.url.replace("/assets", ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user