feat(Community Overview): Trigger chart data updates on specific actions + optimise the backend calls

This commit includes the following changes:
1. Request from backend the messages count in a specific interval as opposed to all messages timestamps in that interval.
2. Update the chart end date before refreshing the data
3. Fix metrics type parsing in community service
4. Fix a bug where the new incoming data was not processed by ChartJs without a hover event on the chart. The fix here is to manually request paint() on model changes.d

Issues found and not handled here:
1. On large communities the backend request can take 3 minutes to complete
2. CPU usage can easily go to 100% when switching chart tabs on large communities. All the requests are processed by the backend.
This commit is contained in:
Alex Jbanca 2023-08-03 23:30:34 +03:00 committed by Alex Jbanca
parent b09504be36
commit 9be2a8d799
12 changed files with 121 additions and 42 deletions

View File

@ -173,11 +173,7 @@ proc init*(self: Controller) =
let args = CommunityMetricsArgs(e)
if args.communityId == self.sectionId:
let metrics = self.communityService.getCommunityMetrics(args.communityId, args.metricsType)
var strings: seq[string]
for interval in metrics.intervals:
for timestamp in interval.timestamps:
strings.add($timestamp)
self.delegate.setOverviewChartData("[" & join(strings, ", ") & "]")
self.delegate.setCommunityMetrics(metrics)
self.events.on(SIGNAL_COMMUNITY_CHANNEL_DELETED) do(e:Args):
let args = CommunityChatIdArgs(e)
@ -664,4 +660,7 @@ proc getCommunityTokenList*(self: Controller): seq[CommunityTokenDto] =
return self.communityTokensService.getCommunityTokens(self.getMySectionId())
proc collectCommunityMetricsMessagesTimestamps*(self: Controller, intervals: string) =
self.communityService.collectCommunityMetricsMessagesTimestamps(self.getMySectionId(), intervals)
self.communityService.collectCommunityMetricsMessagesTimestamps(self.getMySectionId(), intervals)
proc collectCommunityMetricsMessagesCount*(self: Controller, intervals: string) =
self.communityService.collectCommunityMetricsMessagesCount(self.getMySectionId(), intervals)

View File

@ -349,7 +349,10 @@ method deleteCommunityTokenPermission*(self: AccessInterface, communityId: strin
method collectCommunityMetricsMessagesTimestamps*(self: AccessInterface, intervals: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setOverviewChartData*(self: AccessInterface, metrics: string) {.base.} =
method collectCommunityMetricsMessagesCount*(self: AccessInterface, intervals: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setCommunityMetrics*(self: AccessInterface, metrics: CommunityMetricsDto) {.base.} =
raise newException(ValueError, "No implementation available")
method onCommunityTokenPermissionCreated*(self: AccessInterface, communityId: string, tokenPermission: CommunityTokenPermissionDto) {.base.} =

View File

@ -1,4 +1,4 @@
import NimQml, Tables, chronicles, json, sequtils, strutils, strformat, sugar
import NimQml, Tables, chronicles, json, sequtils, strutils, strformat, sugar, marshal
import io_interface
import ../io_interface as delegate_interface
@ -1312,5 +1312,8 @@ method onDeactivateChatLoader*(self: Module, chatId: string) =
method collectCommunityMetricsMessagesTimestamps*(self: Module, intervals: string) =
self.controller.collectCommunityMetricsMessagesTimestamps(intervals)
method setOverviewChartData*(self: Module, metrics: string) =
self.view.setOverviewChartData(metrics)
method setCommunityMetrics*(self: Module, metrics: CommunityMetricsDto) =
self.view.setCommunityMetrics($$metrics)
method collectCommunityMetricsMessagesCount*(self: Module, intervals: string) =
self.controller.collectCommunityMetricsMessagesCount(intervals)

View File

@ -422,9 +422,12 @@ QtObject:
read = getOverviewChartData
notify = overviewChartDataChanged
proc setOverviewChartData*(self: View, communityMetrics: string) =
proc setCommunityMetrics*(self: View, communityMetrics: string) =
self.communityMetrics = communityMetrics
self.overviewChartDataChanged()
proc collectCommunityMetricsMessagesTimestamps*(self: View, intervals: string) {.slot.} =
self.delegate.collectCommunityMetricsMessagesTimestamps(intervals)
proc collectCommunityMetricsMessagesCount*(self: View, intervals: string) {.slot.} =
self.delegate.collectCommunityMetricsMessagesCount(intervals)

View File

@ -338,7 +338,7 @@ proc toCommunityMetricsDto*(jsonObj: JsonNode): CommunityMetricsDto =
result.metricsType = CommunityMetricsType.MessagesTimestamps
var metricsTypeInt: int
if (jsonObj.getProp("metricsType", metricsTypeInt) and (metricsTypeInt >= ord(low(CommunityMetricsType)) and
if (jsonObj.getProp("type", metricsTypeInt) and (metricsTypeInt >= ord(low(CommunityMetricsType)) and
metricsTypeInt <= ord(high(CommunityMetricsType)))):
result.metricsType = CommunityMetricsType(metricsTypeInt)

View File

@ -1379,7 +1379,7 @@ QtObject:
return
let communityId = rpcResponseObj{"communityId"}.getStr()
let metricsType = rpcResponseObj{"metricsType"}.getInt()
let metricsType = rpcResponseObj{"response"}{"result"}{"type"}.getInt()
var metrics = rpcResponseObj{"response"}{"result"}.toCommunityMetricsDto()
self.communityMetrics[communityId] = metrics

View File

@ -4,28 +4,66 @@ import QtQuick.Controls 2.15
import AppLayouts.Communities.panels 1.0
import Models 1.0
import Storybook 1.0
SplitView {
id: root
orientation: Qt.Vertical
OverviewSettingsChart {
id: chart
SplitView.fillWidth: true
SplitView.fillHeight: true
model: generateRandomModel()
onCollectCommunityMetricsMessagesCount: generateRandomModel(intervals)
}
function generateRandomModel() {
function generateRandomModel(intervalsStr) {
if(!intervalsStr) return
var response = {
communityId: "",
metricsType: timestampMetrics.checked ? "MessagesTimestamps" : "MessagesCount",
intervals: []
}
var intervals = JSON.parse(intervalsStr)
response.intervals = intervals.map( x => {
var timestamps = generateRandomDate(x.startTimestamp, x.endTimestamp, Math.random() * 10)
return {
startTimestamp: x.startTimestamp,
endTimestamp: x.endTimestamp,
timestamps: timestamps,
count: timestamps.length
}
})
chart.model = response
}
function generateRandomDate(from, to, count) {
var newModel = []
const now = Date.now()
for(var i = 0; i < 500000; i++) {
var date = generateRandomDate(1463154962000, now)
for(var i = 0; i < count; i++) {
var date = from + Math.random() * (to - from)
newModel.push(date)
}
return newModel
}
function generateRandomDate(from, to) {
return from + Math.random() * (to - from)
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
CheckBox {
id: timestampMetrics
text: "Metrics using timestamps"
checked: false
onCheckedChanged: chart.reset()
}
}
}

View File

@ -27,6 +27,8 @@ Canvas {
function updateToNewData()
{
if(!jsChart) return
jsChart.update('none');
root.requestPaint();
}

View File

@ -423,6 +423,10 @@ QtObject {
chatCommunitySectionModule.collectCommunityMetricsMessagesTimestamps(intervals)
}
function collectCommunityMetricsMessagesCount(intervals) {
chatCommunitySectionModule.collectCommunityMetricsMessagesCount(intervals)
}
function requestCommunityInfo(id, importing = false) {
communitiesModuleInst.requestCommunityInfo(id, importing)
}

View File

@ -17,22 +17,22 @@ StatusChartPanel {
/**
* Flat model to use for the chart containing timestamps
* type: {Array}
* default: []
* example: {"communityId": "", "metricsType": "MessagesCount", "intervals": [{"startTimestamp": 1691047800000, "endTimestamp": 1691062200000, "timestamps": [], "count": 0}]}
*/
property var model: []
signal collectCommunityMetricsMessagesTimestamps(var intervals)
signal collectCommunityMetricsMessagesCount(var intervals)
function requestCommunityMetrics() {
let intervals = d.selectedTabInfo.modelItems.map(item => {
return {
startTimestamp: item.start,
endTimestamp: item.end
}
})
collectCommunityMetricsMessagesTimestamps(JSON.stringify(intervals))
function reset() {
d.now = Date.now()
d.requestCommunityMetrics()
}
onVisibleChanged: if (visible) requestCommunityMetrics()
onVisibleChanged: if(visible) d.resetWithSpamProtection()
onTimeRangeTabBarIndexChanged: reset()
onModelChanged: chart.updateToNewData()
onCollectCommunityMetricsMessagesCount: d.lastRequestModelMetadata = d.selectedTabInfo.modelItems
QtObject {
id: d
@ -49,7 +49,8 @@ StatusChartPanel {
readonly property var hoveredModelMetadata: modelMetadata[root.timeRangeTabBarIndex].modelItems[hoveredBarIndex]
readonly property var tooltipConfig: modelMetadata[root.timeRangeTabBarIndex].tooltipConfig
readonly property var graphTabsModel: [{text: messagesLabel, enabled: true}]
readonly property var now: Date.now()
property var now: Date.now()
property var lastRequestModelMetadata: null
readonly property var chartData: selectedTabInfo.modelItems.map(x => d.itemsCountInRange(root.model, x.start, x.end))
readonly property var labels: selectedTabInfo.modelItems.map(x => x.label)
@ -165,8 +166,35 @@ StatusChartPanel {
}
]
function itemsCountInRange(array, start, end) {
return array ? array.filter(x => x <= end && x > start).length : 0
function resetWithSpamProtection() {
if(Date.now() - d.now > LocaleUtils.minutesToMs(5) || d.lastRequestModelMetadata != selectedTabInfo.modelItems) {
root.reset()
}
}
function requestCommunityMetrics() {
let intervals = d.selectedTabInfo.modelItems.map(item => {
return {
startTimestamp: item.start,
endTimestamp: item.end
}
})
root.collectCommunityMetricsMessagesCount(JSON.stringify(intervals))
}
function itemsCountInRange(model, start, end) {
if (model == undefined || model.intervals == undefined)
return 0
const interval = model.intervals.find(x => x.startTimestamp == start && x.endTimestamp == end)
if (!interval)
return 0
if(model.metricsType === "MessagesTimestamps")
return interval.timestamps.length
return interval.count
}
function minutesStr(before = 0, timeReference = now, roundCurrentTime = true) {
@ -233,9 +261,8 @@ StatusChartPanel {
return leftPositon ? Qt.point(relativeMousePoint.x - toolTip.width - 15, relativeMousePoint.y - 5)
: Qt.point(relativeMousePoint.x + 15, relativeMousePoint.y - 5)
}
onSelectedTabInfoChanged: root.requestCommunityMetrics()
}
headerLeftPadding: 0
headerBottomPadding: Style.current.bigPadding
graphsModel: d.graphTabsModel

View File

@ -49,7 +49,7 @@ StackLayout {
root.currentIndex = 0
}
signal collectCommunityMetricsMessagesTimestamps(var intervals)
signal collectCommunityMetricsMessagesCount(var intervals)
signal edited(Item item) // item containing edited fields (name, description, logoImagePath, color, options, etc..)
@ -118,8 +118,8 @@ StackLayout {
OverviewSettingsChart {
model: JSON.parse(root.overviewChartData)
onCollectCommunityMetricsMessagesTimestamps: {
root.collectCommunityMetricsMessagesTimestamps(intervals)
onCollectCommunityMetricsMessagesCount: {
root.collectCommunityMetricsMessagesCount(intervals)
}
Layout.topMargin: 16
Layout.fillWidth: true
@ -128,7 +128,7 @@ StackLayout {
Connections {
target: root
onCommunityIdChanged: requestCommunityMetrics()
onCommunityIdChanged: reset()
}
}

View File

@ -178,8 +178,8 @@ StatusSectionLayout {
communitySettingsDisabled: root.communitySettingsDisabled
overviewChartData: rootStore.overviewChartData
onCollectCommunityMetricsMessagesTimestamps: {
rootStore.collectCommunityMetricsMessagesTimestamps(intervals)
onCollectCommunityMetricsMessagesCount: {
rootStore.collectCommunityMetricsMessagesCount(intervals)
}
onEdited: {