improve page load performance of large amount urls

1) fix loop to async
2) query n+1 problem
This commit is contained in:
vishal sabhaya 2024-08-17 15:37:40 +09:00
parent bab771df05
commit 84f261c81b
3 changed files with 140 additions and 39 deletions

View File

@ -71,23 +71,15 @@ class Monitor extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @param {object} preloadData Include precalculate data in
* @param {boolean} includeSensitiveData Include sensitive data in * @param {boolean} includeSensitiveData Include sensitive data in
* JSON * JSON
* @returns {Promise<object>} Object ready to parse * @returns {Promise<object>} Object ready to parse
*/ */
async toJSON(includeSensitiveData = true) { async toJSON(preloadData = {}, includeSensitiveData = true) {
let notificationIDList = {}; const tags = preloadData.tags[this.id] || [];
const notificationIDList = preloadData.notifications[this.id] || {};
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();
let screenshot = null; let screenshot = null;
@ -105,15 +97,15 @@ class Monitor extends BeanModel {
path, path,
pathName, pathName,
parent: this.parent, parent: this.parent,
childrenIDs: await Monitor.getAllChildrenIDs(this.id), childrenIDs: preloadData.childrenIDs[this.id] || [],
url: this.url, url: this.url,
method: this.method, method: this.method,
hostname: this.hostname, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries, maxretries: this.maxretries,
weight: this.weight, weight: this.weight,
active: await this.isActive(), active: preloadData.activeStatus[this.id],
forceInactive: !await Monitor.isParentActive(this.id), forceInactive: preloadData.forceInactive[this.id],
type: this.type, type: this.type,
timeout: this.timeout, timeout: this.timeout,
interval: this.interval, interval: this.interval,
@ -134,8 +126,8 @@ class Monitor extends BeanModel {
docker_host: this.docker_host, docker_host: this.docker_host,
proxyId: this.proxy_id, proxyId: this.proxy_id,
notificationIDList, notificationIDList,
tags: tags, tags,
maintenance: await Monitor.isUnderMaintenance(this.id), maintenance: preloadData.maintenanceStatus[this.id],
mqttTopic: this.mqttTopic, mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage, mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType, mqttCheckType: this.mqttCheckType,
@ -199,16 +191,6 @@ class Monitor extends BeanModel {
return data; return data;
} }
/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<boolean>} 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 * Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>} List of tags on the * @returns {Promise<LooseObject<any>[]>} List of tags on the
@ -1178,6 +1160,18 @@ class Monitor extends BeanModel {
return checkCertificateResult; 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<boolean>} Is the monitor active?
*/
static async isActive(monitorID, active) {
const parentActive = await Monitor.isParentActive(monitorID);
return (active === 1) && parentActive;
}
/** /**
* Send statistics to clients * Send statistics to clients
* @param {Server} io Socket server instance * @param {Server} io Socket server instance
@ -1314,7 +1308,10 @@ class Monitor extends BeanModel {
for (let notification of notificationList) { for (let notification of notificationList) {
try { try {
const heartbeatJSON = bean.toJSON(); 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. // Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) { if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A"; heartbeatJSON["msg"] = "N/A";
@ -1325,7 +1322,7 @@ class Monitor extends BeanModel {
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset(); heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT); 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) { } catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name); log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e); 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<LooseObject<any>>} 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<LooseObject<any>>} 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<LooseObject<any>>} 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 * Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get * @param {number} monitorID ID of monitor to get

View File

@ -890,14 +890,17 @@ let needSetup = false;
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`); 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, monitorID,
socket.userID, socket.userID,
]); ]);
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
callback({ callback({
ok: true, ok: true,
monitor: await bean.toJSON(), monitor: await monitor.toJSON(preloadData),
}); });
} catch (e) { } catch (e) {
@ -1644,13 +1647,20 @@ async function afterLogin(socket, user) {
await StatusPage.sendStatusPageList(io, socket); 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) { 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 all combined promises
await Monitor.sendStats(io, monitorID, user.id); await Promise.all(monitorPromises);
}
// Set server timezone from client browser if not set // Set server timezone from client browser if not set
// It should be run once only // It should be run once only

View File

@ -219,9 +219,27 @@ class UptimeKumaServer {
userID, userID,
]); ]);
for (let monitor of monitorList) { // Collect monitor IDs
result[monitor.id] = await monitor.toJSON(); // 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; return result;
} }
@ -520,3 +538,4 @@ const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt"); const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp"); const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb"); const { MongodbMonitorType } = require("./monitor-types/mongodb");
const Monitor = require("./model/monitor");