Reduce issues memory usage

This commit is contained in:
Adrian Woźniak 2021-10-15 13:36:24 +02:00
parent fd4e04647e
commit 5abcfb99a6
No known key found for this signature in database
GPG Key ID: DE43476F72AD3F6C
9 changed files with 176 additions and 149 deletions

View File

@ -2,11 +2,8 @@ use std::collections::HashMap;
use database_actor::issue_assignees::LoadAssignees; use database_actor::issue_assignees::LoadAssignees;
use database_actor::issues::{LoadProjectIssues, UpdateIssue}; use database_actor::issues::{LoadProjectIssues, UpdateIssue};
use jirs_data::msg::{WsMsgIssue, WsMsgProject}; use jirs_data::msg::{IssueSync, WsMsgIssue, WsMsgProject};
use jirs_data::{ use jirs_data::{CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, PayloadVariant, WsMsg};
CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, IssueStatusId, ListPosition,
PayloadVariant, WsMsg,
};
use crate::{db_or_debug_and_return, AsyncHandler, WebSocketActor, WsResult}; use crate::{db_or_debug_and_return, AsyncHandler, WebSocketActor, WsResult};
@ -24,14 +21,12 @@ impl AsyncHandler<WsMsgIssue> for WebSocketActor {
} }
WsMsgIssue::IssueCreate(payload) => self.exec(payload).await, WsMsgIssue::IssueCreate(payload) => self.exec(payload).await,
WsMsgIssue::IssueDelete(id) => self.exec(DeleteIssue { id }).await, WsMsgIssue::IssueDelete(id) => self.exec(DeleteIssue { id }).await,
WsMsgIssue::IssueSyncListPosition(sync) => { WsMsgIssue::IssueSyncListPosition(sync) => self.exec(SyncIssueListPosition(sync)).await,
self.exec(SyncIssueListPosition(sync)).await?;
Ok(None)
}
WsMsgIssue::IssueUpdated(_) => Ok(None), WsMsgIssue::IssueUpdated(_) => Ok(None),
WsMsgIssue::IssueDeleted(_, _) => Ok(None), WsMsgIssue::IssueDeleted(_, _) => Ok(None),
WsMsgIssue::IssueCreated(_) => Ok(None), WsMsgIssue::IssueCreated(_) => Ok(None),
WsMsgIssue::IssueSyncedListPosition(_) => Ok(None),
} }
} }
} }
@ -196,27 +191,37 @@ impl AsyncHandler<LoadIssues> for WebSocketActor {
} }
} }
pub struct SyncIssueListPosition(pub Vec<(IssueId, ListPosition, IssueStatusId, Option<IssueId>)>); pub struct SyncIssueListPosition(pub Vec<IssueSync>);
#[async_trait::async_trait] #[async_trait::async_trait]
impl AsyncHandler<SyncIssueListPosition> for WebSocketActor { impl AsyncHandler<SyncIssueListPosition> for WebSocketActor {
async fn exec(&mut self, msg: SyncIssueListPosition) -> WsResult { async fn exec(&mut self, msg: SyncIssueListPosition) -> WsResult {
let _project_id = self.require_user_project()?.project_id; let _project_id = self.require_user_project()?.project_id;
for (issue_id, list_position, status_id, epic_id) in msg.0 { let mut result = Vec::with_capacity(msg.0.len());
for issue_sync in msg.0 {
crate::actor_or_debug_and_ignore!( crate::actor_or_debug_and_ignore!(
self, self,
db, db,
database_actor::issues::UpdateIssue { database_actor::issues::UpdateIssue {
issue_id, issue_id: issue_sync.id,
list_position: Some(list_position), list_position: Some(issue_sync.list_position),
issue_status_id: Some(status_id), issue_status_id: Some(issue_sync.issue_status_id),
epic_id: Some(epic_id), epic_id: Some(issue_sync.epic_id),
..Default::default() ..Default::default()
}, },
|_| {}; async |issue: database_actor::models::Issue| {
result.push(IssueSync {
id: issue.id,
list_position: issue.list_position,
issue_status_id: issue.issue_status_id,
epic_id: issue.epic_id,
});
}; async
); );
} }
self.exec(LoadIssues).await Ok(Some(WsMsg::Issue(WsMsgIssue::IssueSyncedListPosition(
result,
))))
} }
} }

View File

@ -119,6 +119,14 @@ impl WsError {
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct IssueSync {
pub id: IssueId,
pub list_position: ListPosition,
pub issue_status_id: IssueStatusId,
pub epic_id: Option<IssueId>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum WsMsgIssue { pub enum WsMsgIssue {
IssueUpdate(IssueId, IssueFieldId, PayloadVariant), IssueUpdate(IssueId, IssueFieldId, PayloadVariant),
@ -127,7 +135,8 @@ pub enum WsMsgIssue {
IssueDeleted(IssueId, NumberOfDeleted), IssueDeleted(IssueId, NumberOfDeleted),
IssueCreate(CreateIssuePayload), IssueCreate(CreateIssuePayload),
IssueCreated(Issue), IssueCreated(Issue),
IssueSyncListPosition(Vec<(IssueId, ListPosition, IssueStatusId, Option<IssueId>)>), IssueSyncListPosition(Vec<IssueSync>),
IssueSyncedListPosition(Vec<IssueSync>),
} }
impl From<WsMsgIssue> for WsMsg { impl From<WsMsgIssue> for WsMsg {

View File

@ -169,6 +169,12 @@ macro_rules! match_page {
_ => return, _ => return,
} }
}; };
($model: ident, $ty: ident, $def: expr) => {
match &$model.page_content {
crate::model::PageContent::$ty(page) => page,
_ => return $def,
}
};
($model: ident, $ty: ident; Empty) => { ($model: ident, $ty: ident; Empty) => {
match &$model.page_content { match &$model.page_content {
crate::model::PageContent::$ty(page) => page, crate::model::PageContent::$ty(page) => page,
@ -184,6 +190,12 @@ macro_rules! match_page_mut {
_ => return, _ => return,
} }
}; };
($model: ident, $ty: ident, $def: expr) => {
match &mut $model.page_content {
PageContent::$ty(page) => page,
_ => return $def,
}
};
} }
#[derive(Debug)] #[derive(Debug)]
@ -288,7 +300,7 @@ impl Model {
current_user_project: None, current_user_project: None,
about_tooltip_visible: false, about_tooltip_visible: false,
messages_tooltip_visible: false, messages_tooltip_visible: false,
issues: vec![], issue_ids: vec![],
user_ids: vec![], user_ids: vec![],
users_by_id: HashMap::with_capacity(1_000), users_by_id: HashMap::with_capacity(1_000),
user_settings: None, user_settings: None,
@ -354,15 +366,15 @@ impl Model {
} }
pub fn epic_issue_ids(&self, epic_id: EpicId) -> Vec<IssueId> { pub fn epic_issue_ids(&self, epic_id: EpicId) -> Vec<IssueId> {
self.issues() self.issue_ids
.iter() .iter()
.filter_map(|issue| { .filter(|id| {
if issue.epic_id == Some(epic_id) { self.issues_by_id
Some(issue.id) .get(id)
} else { .filter(|issue| issue.epic_id == Some(epic_id))
None .is_some()
}
}) })
.copied()
.collect() .collect()
} }

View File

@ -39,6 +39,7 @@ impl ProjectPage {
where where
IssueStream: std::iter::Iterator<Item = &'issue Issue>, IssueStream: std::iter::Iterator<Item = &'issue Issue>,
{ {
let num_of_epics = epics.len();
let epics = vec![None].into_iter().chain( let epics = vec![None].into_iter().chain(
epics epics
.iter() .iter()
@ -51,7 +52,7 @@ impl ProjectPage {
&& issue_filter_with_text(issue, page.text_filter.as_str()) && issue_filter_with_text(issue, page.text_filter.as_str())
&& issue_filter_with_only_my(issue, page.only_my_filter, user) && issue_filter_with_only_my(issue, page.only_my_filter, user)
}); });
let issues = if page.recently_updated_filter { let mut issues = if page.recently_updated_filter {
let mut m = HashMap::new(); let mut m = HashMap::new();
let mut sorted: Vec<(IssueId, NaiveDateTime)> = issues let mut sorted: Vec<(IssueId, NaiveDateTime)> = issues
.map(|issue| { .map(|issue| {
@ -71,12 +72,14 @@ impl ProjectPage {
issues.collect() issues.collect()
}; };
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
let issues_per_epic_id = { let issues_per_epic_id = {
let issues_len = issues.len();
issues issues
.into_iter() .into_iter()
.fold(HashMap::with_capacity(issues_len), |mut m, issue| { .fold(HashMap::with_capacity(num_of_epics), |mut m, issue| {
m.entry(issue.epic_id).or_insert_with(Vec::new).push(issue); m.entry(issue.epic_id)
.or_insert_with(|| Vec::with_capacity(100))
.push(issue);
m m
}) })
}; };

View File

@ -5,6 +5,7 @@ use seed::prelude::Orders;
use crate::components::styled_select::StyledSelectChanged; use crate::components::styled_select::StyledSelectChanged;
use crate::model::{Model, Page, PageContent}; use crate::model::{Model, Page, PageContent};
use crate::pages::project_page::model::ProjectPage; use crate::pages::project_page::model::ProjectPage;
use crate::ws::issue::change_visible;
use crate::ws::{board_load, send_ws_msg}; use crate::ws::{board_load, send_ws_msg};
use crate::{ use crate::{
BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, ResourceKind, BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, ResourceKind,
@ -97,7 +98,9 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
issue_bellow_id, issue_bellow_id,
))) => crate::ws::issue::change_position(issue_bellow_id, model), ))) => crate::ws::issue::change_position(issue_bellow_id, model),
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(status))) => { Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(status))) => {
crate::ws::issue::change_status(status, model) if !crate::ws::issue::change_status(status, model) {
orders.skip();
}
} }
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(_status))) => { Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(_status))) => {
crate::ws::issue::sync(model, orders) crate::ws::issue::sync(model, orders)
@ -116,17 +119,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
} }
} }
if rebuild_visible { if rebuild_visible {
let visible_issues = ProjectPage::visible_issues( change_visible(model);
crate::match_page!(model, Project),
model.epics(),
model.issue_statuses(),
model
.issue_ids
.iter()
.filter_map(|id| model.issues_by_id.get(id)),
model.user(),
);
crate::match_page_mut!(model, Project).visible_issues = visible_issues;
} }
} }

View File

@ -118,7 +118,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone( Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone(
_issue_status_id, _issue_status_id,
))) => { ))) => {
sync(model, orders); // sync(model, orders);
} }
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName( Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(
id, id,

View File

@ -209,7 +209,6 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
.issue_ids .issue_ids
.iter() .iter()
.filter_map(|id| model.issues_by_id.get(id)) .filter_map(|id| model.issues_by_id.get(id))
.iter()
.fold( .fold(
HashMap::with_capacity(model.issue_statuses_by_id.len()), HashMap::with_capacity(model.issue_statuses_by_id.len()),
|mut h, issue| { |mut h, issue| {

View File

@ -1,4 +1,4 @@
use jirs_data::msg::WsMsgIssue; use jirs_data::msg::{IssueSync, WsMsgIssue};
use jirs_data::*; use jirs_data::*;
use seed::prelude::Orders; use seed::prelude::Orders;
use seed::*; use seed::*;
@ -30,59 +30,51 @@ pub fn change_position(below_id: EpicId, model: &mut Model) {
} }
let (issue_status_id, epic_id) = model let (issue_status_id, epic_id) = model
.issues_by_id
.get(&dragged_id)
.map(|issue| (issue.issue_status_id, issue.epic_id))
.unwrap_or_default();
let mut issues = model
.issue_ids .issue_ids
.iter() .iter()
.filter_map(|id| model.issues_by_id.get(id)) .filter_map(|id| {
.find_map(|issue| { model
if issue.id == dragged_id { .issues_by_id
Some((issue.issue_status_id, issue.epic_id)) .get(id)
} else { .filter(|issue| {
None issue.issue_status_id == issue_status_id && issue.epic_id == epic_id
} })
.map(|issue| (issue.id, issue.list_position))
}) })
.unwrap_or_default(); .collect::<Vec<(i32, i32)>>();
issues.sort_by(|(_, a), (_, b)| a.cmp(b));
let mut issues: Vec<Issue> = model {
.issues_mut() let below_idx = issues
.drain_filter(|issue| issue.issue_status_id == issue_status_id && issue.epic_id == epic_id) .iter()
.collect(); .position(|(id, _)| *id == below_id)
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); .unwrap_or_default();
let dragged_idx = issues
let below_idx = issues .iter()
.iter() .position(|(id, _)| *id == dragged_id)
.position(|issue| issue.id == below_id) .unwrap_or_default();
.unwrap_or_default(); let dragged = issues.remove(dragged_idx);
let dragged_idx = issues issues.insert(below_idx, dragged);
.iter()
.position(|issue| issue.id == dragged_id)
.unwrap_or_default();
let dragged = issues.remove(dragged_idx);
issues.insert(below_idx, dragged);
let mut changed = Vec::with_capacity(issues.len());
for (idx, mut issue) in issues.into_iter().enumerate() {
issue.list_position = idx as i32;
if let Some(iss) = model.issues_by_id.get_mut(&issue.id) {
iss.list_position = issue.list_position;
}
changed.push((issue.id, issue.list_position));
model.issues_mut().push(issue);
} }
let visible = ProjectPage::visible_issues( {
crate::match_page!(model, Project), for (idx, (id, _)) in issues.into_iter().enumerate() {
model.epics(), if let Some(issue) = model.issues_by_id.get_mut(&id) {
model.issue_statuses(), issue.list_position = idx as i32;
model.issues(), }
model.user(), crate::match_page_mut!(model, Project)
); .issue_drag
if let PageContent::Project(project_page) = &mut model.page_content { .mark_dirty(id);
project_page.visible_issues = visible;
for (id, _) in changed.iter() {
project_page.issue_drag.mark_dirty(*id);
} }
} }
change_visible(model);
} }
pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -91,19 +83,17 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
_ => return, _ => return,
}; };
let changes: Vec<(EpicId, ListPosition, IssueStatusId, Option<EpicId>)> = dirty let changes = dirty
.into_iter() .into_iter()
.filter_map(|id| { .filter_map(|id| {
model.issues_by_id.get(&id).map(|issue| { model.issues_by_id.get(&id).map(|issue| IssueSync {
( id: issue.id,
issue.id, list_position: issue.list_position,
issue.list_position, issue_status_id: issue.issue_status_id,
issue.issue_status_id, epic_id: issue.epic_id,
issue.epic_id,
)
}) })
}) })
.collect(); .collect::<Vec<IssueSync>>();
send_ws_msg( send_ws_msg(
WsMsg::Issue(WsMsgIssue::IssueSyncListPosition(changes)), WsMsg::Issue(WsMsgIssue::IssueSyncListPosition(changes)),
@ -113,15 +103,18 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
crate::match_page_mut!(model, Project).issue_drag.clear(); crate::match_page_mut!(model, Project).issue_drag.clear();
} }
pub fn change_status(status_id: IssueStatusId, model: &mut Model) { pub fn change_status(status_id: IssueStatusId, model: &mut Model) -> bool {
let dragged_id = match crate::match_page!(model, Project) let dragged_id = match crate::match_page!(model, Project, false)
.issue_drag .issue_drag
.dragged_id .dragged_id
.as_ref() .as_ref()
.cloned() .cloned()
{ {
Some(issue_id) => issue_id, Some(issue_id) => issue_id,
_ => return error!("Nothing is dragged"), _ => {
error!("Nothing is dragged");
return false;
}
}; };
let (issue_status_id, epic_id) = model let (issue_status_id, epic_id) = model
.issues_by_id .issues_by_id
@ -129,45 +122,50 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
.map(|issue| (issue.issue_status_id, issue.epic_id)) .map(|issue| (issue.issue_status_id, issue.epic_id))
.unwrap_or_default(); .unwrap_or_default();
if status_id == issue_status_id { if status_id == issue_status_id {
return; return false;
} }
let mut issues = { let epic_and_status_issues_count = model
let mut h = std::mem::take(&mut model.issues_by_id); .issue_ids
h.keys() .iter()
.filter_map(|id| h.remove(id)) .filter_map(|id| {
.collect::<Vec<Issue>>() model
} .issues_by_id
.drain_filter(|issue| { .get(id)
if issue.id == dragged_id { .filter(|issue| same_epic_and_status(status_id, epic_id, issue))
issue.issue_status_id = status_id; })
} .count();
issue.issue_status_id == status_id && issue.epic_id == epic_id
})
.collect::<Vec<Issue>>();
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
let mut dirty = vec![];
for mut issue in issues {
if issue.id == dragged_id {
issue.issue_status_id = status_id;
if let Some(iss) = model.issues_by_id.get_mut(&issue.id) {
iss.issue_status_id = status_id;
}
}
dirty.push(issue.id);
model.issue_ids.push(issue.id);
model.issues_by_id.insert(issue.id, issue);
}
{ {
let project_page = crate::match_page_mut!(model, Project); if let Some(issue) = model.issues_by_id.get_mut(&dragged_id) {
for id in dirty { issue.issue_status_id = status_id;
issue.list_position = epic_and_status_issues_count as ListPosition;
}
}
let issues_in_column = model
.issue_ids
.iter()
.filter_map(|id| {
model
.issues_by_id
.get(id)
.filter(|issue| same_epic_and_status(status_id, epic_id, issue))
.map(|issue| issue.id)
})
.collect::<Vec<i32>>();
{
let project_page = crate::match_page_mut!(model, Project, false);
for id in issues_in_column {
project_page.issue_drag.mark_dirty(id); project_page.issue_drag.mark_dirty(id);
} }
} }
change_visible(model);
true
}
pub fn change_visible(model: &mut Model) {
let visible = ProjectPage::visible_issues( let visible = ProjectPage::visible_issues(
crate::match_page!(model, Project), crate::match_page!(model, Project),
model.epics(), model.epics(),
@ -181,3 +179,7 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
crate::match_page_mut!(model, Project).visible_issues = visible; crate::match_page_mut!(model, Project).visible_issues = visible;
} }
fn same_epic_and_status(status_id: IssueStatusId, epic_id: Option<EpicId>, issue: &&Issue) -> bool {
issue.issue_status_id == status_id && issue.epic_id == epic_id
}

View File

@ -253,21 +253,16 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
// issues // issues
WsMsg::Project(WsMsgProject::ProjectIssuesLoaded(v)) => { WsMsg::Project(WsMsgProject::ProjectIssuesLoaded(v)) => {
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64))); v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
{ let len = v.len();
model.issue_ids = Vec::with_capacity(v.len()); model.issue_ids = Vec::with_capacity(len);
model.issues_by_id = model.issues_by_id =
std::mem::take(v) std::mem::take(v)
.into_iter() .into_iter()
.fold(HashMap::with_capacity(v.len()), |h, o| { .fold(HashMap::with_capacity(len), |mut h, o| {
model.issue_ids.push(o.id); model.issue_ids.push(o.id);
h.insert(o.id, o); h.insert(o.id, o);
h h
}); });
};
model.issues_by_id.clear();
for issue in v {
model.issues_by_id.insert(issue.id, issue.clone());
}
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue, ResourceKind::Issue,
@ -312,6 +307,15 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
Some(*id), Some(*id),
)); ));
} }
WsMsg::Issue(WsMsgIssue::IssueSyncedListPosition(sync)) => {
for o in sync {
if let Some(issue) = model.issues_by_id.get_mut(&o.id) {
issue.list_position = o.list_position;
issue.issue_status_id = o.issue_status_id;
issue.epic_id = o.epic_id;
}
}
}
// users // users
WsMsg::Project(WsMsgProject::ProjectUsersLoaded(v)) => { WsMsg::Project(WsMsgProject::ProjectUsersLoaded(v)) => {
model.user_ids = v.iter().map(|u| u.id).collect(); model.user_ids = v.iter().map(|u| u.id).collect();