From 84f261c81bd94ca67fe8be71a437b92d71794e3a Mon Sep 17 00:00:00 2001 From: vishal sabhaya Date: Sat, 17 Aug 2024 15:37:40 +0900 Subject: [PATCH] improve page load performance of large amount urls 1) fix loop to async 2) query n+1 problem --- server/model/monitor.js | 130 +++++++++++++++++++++++++++-------- server/server.js | 24 +++++-- server/uptime-kuma-server.js | 25 ++++++- 3 files changed, 140 insertions(+), 39 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index a5c290c3..8ec09b4a 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -71,23 +71,15 @@ class Monitor extends BeanModel { /** * Return an object that ready to parse to JSON + * @param {object} preloadData Include precalculate data in * @param {boolean} includeSensitiveData Include sensitive data in * JSON * @returns {Promise} Object ready to parse */ - async toJSON(includeSensitiveData = true) { + async toJSON(preloadData = {}, includeSensitiveData = true) { - let notificationIDList = {}; - - let list = await R.find("monitor_notification", " monitor_id = ? ", [ - this.id, - ]); - - for (let bean of list) { - notificationIDList[bean.notification_id] = true; - } - - const tags = await this.getTags(); + const tags = preloadData.tags[this.id] || []; + const notificationIDList = preloadData.notifications[this.id] || {}; let screenshot = null; @@ -105,15 +97,15 @@ class Monitor extends BeanModel { path, pathName, parent: this.parent, - childrenIDs: await Monitor.getAllChildrenIDs(this.id), + childrenIDs: preloadData.childrenIDs[this.id] || [], url: this.url, method: this.method, hostname: this.hostname, port: this.port, maxretries: this.maxretries, weight: this.weight, - active: await this.isActive(), - forceInactive: !await Monitor.isParentActive(this.id), + active: preloadData.activeStatus[this.id], + forceInactive: preloadData.forceInactive[this.id], type: this.type, timeout: this.timeout, interval: this.interval, @@ -134,8 +126,8 @@ class Monitor extends BeanModel { docker_host: this.docker_host, proxyId: this.proxy_id, notificationIDList, - tags: tags, - maintenance: await Monitor.isUnderMaintenance(this.id), + tags, + maintenance: preloadData.maintenanceStatus[this.id], mqttTopic: this.mqttTopic, mqttSuccessMessage: this.mqttSuccessMessage, mqttCheckType: this.mqttCheckType, @@ -199,16 +191,6 @@ class Monitor extends BeanModel { return data; } - /** - * Checks if the monitor is active based on itself and its parents - * @returns {Promise} Is the monitor active? - */ - async isActive() { - const parentActive = await Monitor.isParentActive(this.id); - - return (this.active === 1) && parentActive; - } - /** * Get all tags applied to this monitor * @returns {Promise[]>} List of tags on the @@ -1178,6 +1160,18 @@ class Monitor extends BeanModel { return checkCertificateResult; } + /** + * Checks if the monitor is active based on itself and its parents + * @param {number} monitorID ID of monitor to send + * @param {boolean} active is active + * @returns {Promise} Is the monitor active? + */ + static async isActive(monitorID, active) { + const parentActive = await Monitor.isParentActive(monitorID); + + return (active === 1) && parentActive; + } + /** * Send statistics to clients * @param {Server} io Socket server instance @@ -1314,7 +1308,10 @@ class Monitor extends BeanModel { for (let notification of notificationList) { try { const heartbeatJSON = bean.toJSON(); - + const monitorData = [{ id: monitor.id, + active: monitor.active + }]; + const preloadData = await Monitor.preparePreloadData(monitorData); // Prevent if the msg is undefined, notifications such as Discord cannot send out. if (!heartbeatJSON["msg"]) { heartbeatJSON["msg"] = "N/A"; @@ -1325,7 +1322,7 @@ class Monitor extends BeanModel { heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset(); heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT); - await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON); + await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(preloadData, false), heartbeatJSON); } catch (e) { log.error("monitor", "Cannot send notification to " + notification.name); log.error("monitor", e); @@ -1487,6 +1484,81 @@ class Monitor extends BeanModel { } } + /** + * Gets monitor notification of multiple monitor + * @param {Array} monitorIDs IDs of monitor to get + * @returns {Promise>} object + */ + static async getMonitorNotification(monitorIDs) { + return await R.getAll(` + SELECT monitor_notification.monitor_id, monitor_notification.notification_id + FROM monitor_notification + WHERE monitor_notification.monitor_id IN (?) + `, [ + monitorIDs, + ]); + } + + /** + * Gets monitor tags of multiple monitor + * @param {Array} monitorIDs IDs of monitor to get + * @returns {Promise>} object + */ + static async getMonitorTag(monitorIDs) { + return await R.getAll(` + SELECT monitor_tag.monitor_id, tag.name, tag.color + FROM monitor_tag + JOIN tag ON monitor_tag.tag_id = tag.id + WHERE monitor_tag.monitor_id IN (?) + `, [ + monitorIDs, + ]); + } + + /** + * prepare preloaded data for efficient access + * @param {Array} monitorData IDs & active field of monitor to get + * @returns {Promise>} object + */ + static async preparePreloadData(monitorData) { + const monitorIDs = monitorData.map(monitor => monitor.id); + const notifications = await Monitor.getMonitorNotification(monitorIDs); + const tags = await Monitor.getMonitorTag(monitorIDs); + const maintenanceStatuses = await Promise.all( + monitorData.map(monitor => Monitor.isUnderMaintenance(monitor.id)) + ); + const childrenIDs = await Promise.all( + monitorData.map(monitor => Monitor.getAllChildrenIDs(monitor.id)) + ); + const activeStatuses = await Promise.all( + monitorData.map(monitor => Monitor.isActive(monitor.id, monitor.active)) + ); + const forceInactiveStatuses = await Promise.all( + monitorData.map(monitor => Monitor.isParentActive(monitor.id)) + ); + + // Organize preloaded data for efficient access + return { + notifications: notifications.reduce((acc, row) => { + acc[row.monitor_id] = acc[row.monitor_id] || {}; + acc[row.monitor_id][row.notification_id] = true; + return acc; + }, {}), + tags: tags.reduce((acc, row) => { + acc[row.monitor_id] = acc[row.monitor_id] || []; + acc[row.monitor_id].push({ name: row.name, + color: row.color + }); + return acc; + }, {}), + maintenanceStatus: Object.fromEntries(monitorData.map((m, index) => [ m.id, maintenanceStatuses[index] ])), + childrenIDs: Object.fromEntries(monitorData.map((m, index) => [ m.id, childrenIDs[index] ])), + activeStatus: Object.fromEntries(monitorData.map((m, index) => [ m.id, activeStatuses[index] ])), + forceInactive: Object.fromEntries(monitorData.map((m, index) => [ m.id, !forceInactiveStatuses[index] ])), + }; + + } + /** * Gets Parent of the monitor * @param {number} monitorID ID of monitor to get diff --git a/server/server.js b/server/server.js index d0af4038..3365fef4 100644 --- a/server/server.js +++ b/server/server.js @@ -890,14 +890,17 @@ let needSetup = false; log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`); - let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [ + let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [ monitorID, socket.userID, ]); - + const monitorData = [{ id: monitor.id, + active: monitor.active + }]; + const preloadData = await Monitor.preparePreloadData(monitorData); callback({ ok: true, - monitor: await bean.toJSON(), + monitor: await monitor.toJSON(preloadData), }); } catch (e) { @@ -1644,13 +1647,20 @@ async function afterLogin(socket, user) { await StatusPage.sendStatusPageList(io, socket); + // Create an array to store the combined promises for both sendHeartbeatList and sendStats + const monitorPromises = []; for (let monitorID in monitorList) { - await sendHeartbeatList(socket, monitorID); + // Combine both sendHeartbeatList and sendStats for each monitor into a single Promise + monitorPromises.push( + Promise.all([ + sendHeartbeatList(socket, monitorID), + Monitor.sendStats(io, monitorID, user.id) + ]) + ); } - for (let monitorID in monitorList) { - await Monitor.sendStats(io, monitorID, user.id); - } + // Await all combined promises + await Promise.all(monitorPromises); // Set server timezone from client browser if not set // It should be run once only diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 573d791a..d0fc54e9 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -219,9 +219,27 @@ class UptimeKumaServer { userID, ]); - for (let monitor of monitorList) { - result[monitor.id] = await monitor.toJSON(); - } + // Collect monitor IDs + // Create monitorData with id, active + const monitorData = monitorList.map(monitor => ({ + id: monitor.id, + active: monitor.active, + })); + const preloadData = await Monitor.preparePreloadData(monitorData); + + // Create an array of promises to convert each monitor to JSON in parallel + const monitorPromises = monitorList.map(monitor => monitor.toJSON(preloadData).then(json => { + return { id: monitor.id, + json + }; + })); + // Wait for all promises to resolve + const monitors = await Promise.all(monitorPromises); + + // Populate the result object with monitor IDs as keys, JSON objects as values + monitors.forEach(monitor => { + result[monitor.id] = monitor.json; + }); return result; } @@ -520,3 +538,4 @@ const { DnsMonitorType } = require("./monitor-types/dns"); const { MqttMonitorType } = require("./monitor-types/mqtt"); const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); +const Monitor = require("./model/monitor");