Skip to content

Commit 4db19f5

Browse files
committed
perf: Refactor the code to use interning and deduplication to reduce CPU usage by over 300%
Added two interners: one slotmap with a hashmap for deduplication, and a trie-based string interner that prevent large moves of stuff like `Issue`s and the duplication of frequent structs like `Author`. Each component now has a `Arc<RwLock<UiIssuePool>>`, which is used to add and query issues from the global interning system. Runtime performance and snappiness seem equivalent to previous versions refactor perf: changed IssueList's IssueListItem to store a IssueId refactor: remove n_links field from interner fix: remove the issue about duplication in UiIssuePool
1 parent c451a7e commit 4db19f5

8 files changed

Lines changed: 596 additions & 118 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ cli-clipboard = "0.4.0"
5353
tachyonfx = { version = "0.24.0", default-features = false, features = ["std", "std-duration"] }
5454
serde = { version = "1.0.228", features = ["derive"] }
5555
serde_json = "1.0.149"
56+
slotmap = "1.0.7"
5657

5758
[profile.release]
5859
codegen-units = 1 # Allows compiler to perform better optimization.

src/ui/components/issue_conversation.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use ratatui::{
2626
use ratatui_macros::{horizontal, line, span, vertical};
2727
use std::{
2828
collections::{HashMap, HashSet},
29-
sync::{Arc, OnceLock},
29+
sync::{Arc, OnceLock, RwLock},
3030
};
3131
use syntect::{
3232
easy::HighlightLines,
@@ -47,6 +47,7 @@ use crate::{
4747
help::HelpElementKind,
4848
issue_list::{IssueClosePopupState, MainScreen, render_issue_close_popup},
4949
},
50+
issue_data::{UiIssue, UiIssuePool},
5051
layout::Layout,
5152
toast_action,
5253
utils::get_border_style,
@@ -115,6 +116,19 @@ impl IssueConversationSeed {
115116
title: Some(Arc::<str>::from(issue.title.as_str())),
116117
}
117118
}
119+
120+
pub fn from_ui_issue(issue: &UiIssue, pool: &UiIssuePool) -> Self {
121+
Self {
122+
number: issue.number,
123+
author: Arc::<str>::from(pool.author_login(issue.author)),
124+
created_at: Arc::<str>::from(pool.resolve_str(issue.created_at_short)),
125+
created_ts: issue.created_ts,
126+
body: issue
127+
.body
128+
.map(|body| Arc::<str>::from(pool.resolve_str(body))),
129+
title: Some(Arc::<str>::from(pool.resolve_str(issue.title))),
130+
}
131+
}
118132
}
119133

120134
#[derive(Debug, Clone)]
@@ -217,6 +231,7 @@ pub struct IssueConversation {
217231
owner: String,
218232
repo: String,
219233
current_user: String,
234+
issue_pool: Arc<RwLock<UiIssuePool>>,
220235
list_state: ListState<RowSelection>,
221236
message_keys: Vec<MessageKey>,
222237
show_timeline: bool,
@@ -293,7 +308,7 @@ impl IssueConversation {
293308
)
294309
}
295310

296-
pub fn new(app_state: crate::ui::AppState) -> Self {
311+
pub fn new(app_state: crate::ui::AppState, issue_pool: Arc<RwLock<UiIssuePool>>) -> Self {
297312
Self {
298313
title: None,
299314
action_tx: None,
@@ -318,6 +333,7 @@ impl IssueConversation {
318333
owner: app_state.owner,
319334
repo: app_state.repo,
320335
current_user: app_state.current_user,
336+
issue_pool,
321337
list_state: ListState::default(),
322338
message_keys: Vec::new(),
323339
show_timeline: false,
@@ -869,6 +885,7 @@ impl IssueConversation {
869885
};
870886
let owner = self.owner.clone();
871887
let repo = self.repo.clone();
888+
let issue_pool = self.issue_pool.clone();
872889
tokio::spawn(async move {
873890
let Some(client) = GITHUB_CLIENT.get() else {
874891
let _ = action_tx
@@ -888,11 +905,12 @@ impl IssueConversation {
888905
.await
889906
{
890907
Ok(issue) => {
891-
let _ = action_tx
892-
.send(Action::IssueCloseSuccess {
893-
issue: Box::new(issue),
894-
})
895-
.await;
908+
let issue_id = {
909+
let mut pool = issue_pool.write().expect("issue pool lock poisoned");
910+
let compact = UiIssue::from_octocrab(&issue, &mut pool);
911+
pool.upsert_issue(compact)
912+
};
913+
let _ = action_tx.send(Action::IssueCloseSuccess { issue_id }).await;
896914
}
897915
Err(err) => {
898916
let _ = action_tx
@@ -1868,22 +1886,27 @@ impl Component for IssueConversation {
18681886
self.markdown_cache.remove(&existing.id);
18691887
}
18701888
}
1871-
Action::IssueCloseSuccess { issue } => {
1872-
let issue = *issue;
1889+
Action::IssueCloseSuccess { issue_id } => {
1890+
let (issue_number, preview_seed) = {
1891+
let pool = self.issue_pool.read().expect("issue pool lock poisoned");
1892+
let issue = pool.get_issue(issue_id);
1893+
(
1894+
issue.number,
1895+
crate::ui::components::issue_detail::IssuePreviewSeed::from_ui_issue(
1896+
issue, &pool,
1897+
),
1898+
)
1899+
};
18731900
let initiated_here = self
18741901
.close_popup
18751902
.as_ref()
1876-
.is_some_and(|popup| popup.issue_number == issue.number);
1903+
.is_some_and(|popup| popup.issue_number == issue_number);
18771904
if initiated_here {
18781905
self.close_popup = None;
18791906
self.close_error = None;
18801907
if let Some(action_tx) = self.action_tx.as_ref() {
18811908
let _ = action_tx
1882-
.send(Action::SelectedIssuePreview {
1883-
seed: crate::ui::components::issue_detail::IssuePreviewSeed::from_issue(
1884-
&issue,
1885-
),
1886-
})
1909+
.send(Action::SelectedIssuePreview { seed: preview_seed })
18871910
.await;
18881911
let _ = action_tx.send(Action::RefreshIssueList).await;
18891912
}

src/ui/components/issue_create.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use async_trait::async_trait;
22
use crossterm::event;
3-
use octocrab::models::issues::Issue;
43
use rat_cursor::HasScreenCursor;
54
use rat_widget::{
65
event::{HandleEvent, TextOutcome, ct_event},
@@ -30,13 +29,15 @@ use crate::{
3029
issue_detail::IssuePreviewSeed,
3130
issue_list::MainScreen,
3231
},
32+
issue_data::{IssueId, UiIssue, UiIssuePool},
3333
layout::Layout,
3434
toast_action,
3535
utils::get_border_style,
3636
},
3737
};
3838
use anyhow::anyhow;
3939
use ratatui_toaster::ToastType;
40+
use std::sync::{Arc, RwLock};
4041

4142
pub const HELP: &[HelpElementKind] = &[
4243
crate::help_text!("Issue Create Help"),
@@ -67,6 +68,7 @@ pub struct IssueCreate {
6768
action_tx: Option<tokio::sync::mpsc::Sender<Action>>,
6869
owner: String,
6970
repo: String,
71+
issue_pool: Arc<RwLock<UiIssuePool>>,
7072
screen: MainScreen,
7173
focus: FocusFlag,
7274
area: Rect,
@@ -86,11 +88,15 @@ pub struct IssueCreate {
8688
}
8789

8890
impl IssueCreate {
89-
pub fn new(AppState { owner, repo, .. }: AppState) -> Self {
91+
pub fn new(
92+
AppState { owner, repo, .. }: AppState,
93+
issue_pool: Arc<RwLock<UiIssuePool>>,
94+
) -> Self {
9095
Self {
9196
action_tx: None,
9297
owner,
9398
repo,
99+
issue_pool,
94100
screen: MainScreen::List,
95101
focus: FocusFlag::new().with_name("issue_create"),
96102
area: Rect::default(),
@@ -171,6 +177,7 @@ impl IssueCreate {
171177
};
172178
let owner = self.owner.clone();
173179
let repo = self.repo.clone();
180+
let issue_pool = self.issue_pool.clone();
174181
self.creating = true;
175182
self.error = None;
176183

@@ -197,10 +204,13 @@ impl IssueCreate {
197204

198205
match create.send().await {
199206
Ok(issue) => {
207+
let issue_id = {
208+
let mut pool = issue_pool.write().expect("issue pool lock poisoned");
209+
let compact = UiIssue::from_octocrab(&issue, &mut pool);
210+
pool.upsert_issue(compact)
211+
};
200212
let _ = action_tx
201-
.send(Action::IssueCreateSuccess {
202-
issue: Box::new(issue),
203-
})
213+
.send(Action::IssueCreateSuccess { issue_id })
204214
.await;
205215
let _ = action_tx
206216
.send(toast_action(
@@ -223,16 +233,22 @@ impl IssueCreate {
223233
});
224234
}
225235

226-
async fn handle_create_success(&mut self, issue: Issue) {
236+
async fn handle_create_success(&mut self, issue_id: IssueId) {
227237
self.creating = false;
228238
self.error = None;
229239
let Some(action_tx) = self.action_tx.clone() else {
230240
return;
231241
};
232-
let number = issue.number;
233-
let labels = issue.labels.clone();
234-
let preview_seed = IssuePreviewSeed::from_issue(&issue);
235-
let conversation_seed = IssueConversationSeed::from_issue(&issue);
242+
let (number, labels, preview_seed, conversation_seed) = {
243+
let pool = self.issue_pool.read().expect("issue pool lock poisoned");
244+
let issue = pool.get_issue(issue_id);
245+
(
246+
issue.number,
247+
issue.labels.clone(),
248+
IssuePreviewSeed::from_ui_issue(issue, &pool),
249+
IssueConversationSeed::from_ui_issue(issue, &pool),
250+
)
251+
};
236252
self.reset_form();
237253
let _ = action_tx
238254
.send(Action::SelectedIssue { number, labels })
@@ -440,9 +456,9 @@ impl Component for IssueCreate {
440456
self.screen = MainScreen::CreateIssue;
441457
self.reset_form();
442458
}
443-
Action::IssueCreateSuccess { issue } => {
459+
Action::IssueCreateSuccess { issue_id } => {
444460
if self.screen == MainScreen::CreateIssue {
445-
self.handle_create_success(*issue).await;
461+
self.handle_create_success(issue_id).await;
446462
}
447463
}
448464
Action::IssueCreateError { message } => {

src/ui/components/issue_detail.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ use ratatui_macros::line;
1414

1515
use crate::{
1616
errors::AppError,
17-
ui::{Action, AppState, components::DumbComponent, layout::Layout},
17+
ui::{
18+
Action, AppState,
19+
components::DumbComponent,
20+
issue_data::{UiIssue, UiIssuePool},
21+
layout::Layout,
22+
},
1823
};
1924
use hyperrat::Link;
2025

@@ -59,6 +64,31 @@ impl IssuePreviewSeed {
5964
.map(|pr| Arc::<str>::from(pr.html_url.as_str())),
6065
}
6166
}
67+
68+
pub fn from_ui_issue(issue: &UiIssue, pool: &UiIssuePool) -> Self {
69+
let assignees = issue
70+
.assignees
71+
.iter()
72+
.map(|assignee| Arc::<str>::from(pool.author_login(*assignee)))
73+
.collect();
74+
let milestone = issue
75+
.milestone
76+
.map(|milestone| Arc::<str>::from(pool.resolve_str(milestone)));
77+
Self {
78+
number: issue.number,
79+
state: issue.state.clone(),
80+
author: Arc::<str>::from(pool.author_login(issue.author)),
81+
created_at: Arc::<str>::from(pool.resolve_str(issue.created_at_short)),
82+
updated_at: Arc::<str>::from(pool.resolve_str(issue.updated_at_short)),
83+
comments: issue.comments,
84+
assignees,
85+
milestone,
86+
is_pull_request: issue.is_pull_request,
87+
pull_request_url: issue
88+
.pull_request_url
89+
.map(|url| Arc::<str>::from(pool.resolve_str(url))),
90+
}
91+
}
6292
}
6393

6494
#[derive(Debug, Clone)]

0 commit comments

Comments
 (0)