Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve page load performance of large amount urls #5025

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 101 additions & 29 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,15 @@ class Monitor extends BeanModel {

/**
* Return an object that ready to parse to JSON
* @param {object} preloadData Include precalculate data in
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
* @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {Promise<object>} Object ready to parse
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
*/
async toJSON(includeSensitiveData = true) {
async toJSON(preloadData = {}, includeSensitiveData = true) {
vishalsabhaya marked this conversation as resolved.
Show resolved Hide resolved

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;

Expand All @@ -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,
Expand All @@ -134,8 +126,8 @@ class Monitor extends BeanModel {
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
tags,
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
maintenance: preloadData.maintenanceStatus[this.id],
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType,
Expand Down Expand Up @@ -199,16 +191,6 @@ class Monitor extends BeanModel {
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
* @returns {Promise<LooseObject<any>[]>} List of tags on the
Expand Down Expand Up @@ -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<boolean>} 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
Expand Down Expand Up @@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the .reduce calls with object-changes here are really hard to parse for me and thus likely other devs.
If there were a bug in here, I would not be able to tell.

Please move this into regular for-loops.
While moving this, please also check if Map is not more appropriate.
I found this blog post with some claims that this might increase performance (if that code happends to be a factor we are spending a part of the remaining 700ms..)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CommanderStorm
understand let me double check with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CommanderStorm
I simplified logic. can you review one more time.

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
Expand Down
41 changes: 25 additions & 16 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
}

const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const { sleep, log, getRandomInt, genSecret, isDev, OPERATIONS } = require("../src/util");
const config = require("./config");

log.debug("server", "Arguments");
Expand Down Expand Up @@ -695,7 +695,7 @@ let needSetup = false;

await updateMonitorNotification(bean.id, notificationIDList);

await server.sendMonitorList(socket);
await server.sendMonitorList(socket, OPERATIONS.ADD, bean.id);

if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
Expand Down Expand Up @@ -846,11 +846,11 @@ let needSetup = false;

await updateMonitorNotification(bean.id, monitor.notificationIDList);

if (await bean.isActive()) {
if (await Monitor.isActive(bean.id, bean.active)) {
await restartMonitor(socket.userID, bean.id);
}

await server.sendMonitorList(socket);
await server.sendMonitorList(socket, OPERATIONS.UPDATE, bean.id);

callback({
ok: true,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -948,7 +951,7 @@ let needSetup = false;
try {
checkLogin(socket);
await startMonitor(socket.userID, monitorID);
await server.sendMonitorList(socket);
await server.sendMonitorList(socket, OPERATIONS.UPDATE, monitorID);

callback({
ok: true,
Expand All @@ -968,7 +971,7 @@ let needSetup = false;
try {
checkLogin(socket);
await pauseMonitor(socket.userID, monitorID);
await server.sendMonitorList(socket);
await server.sendMonitorList(socket, OPERATIONS.UPDATE, monitorID);

callback({
ok: true,
Expand Down Expand Up @@ -1014,8 +1017,7 @@ let needSetup = false;
msg: "successDeleted",
msgi18n: true,
});

await server.sendMonitorList(socket);
await server.sendMonitorList(socket, OPERATIONS.DELETE, monitorID);

} catch (e) {
callback({
Expand Down Expand Up @@ -1644,13 +1646,20 @@ async function afterLogin(socket, user) {

await StatusPage.sendStatusPageList(io, socket);

for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
// Create an array to store the combined promises for both sendHeartbeatList and sendStats
const monitorPromises = [];
for (let monitorID in monitorList.list) {
// Combine both sendHeartbeatList and sendStats for each monitor into a single Promise
monitorPromises.push(
Promise.all([
vishalsabhaya marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
61 changes: 50 additions & 11 deletions server/uptime-kuma-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log, isDev } = require("../src/util");
const { log, isDev, OPERATIONS } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { Settings } = require("./settings");
Expand Down Expand Up @@ -197,32 +197,70 @@ class UptimeKumaServer {
/**
* Send list of monitors to client
* @param {Socket} socket Socket to send list on
* @param {string} op list, add, update, delete
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<object>} List of monitors
*/
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list);
return list;
async sendMonitorList(socket, op = OPERATIONS.LIST, monitorID = null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems pretty strange
the whole part why you added op here and above seems to be to create a fast-path for OPERATIONS.DELETE, right?

Instead of doing this, please just remove the calls for OPERATIONS.DELETE if we really don't use the list there..
Cleans up the code a lot..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CommanderStorm
its used as common method. based on this parameter we will identified operation & update same to frontend list. you can refer socket.js changes

while page load it will use OPERATIONS.LIST as default.
while insert or update. we will add only 1 URL object to list.
while delete no need to send list because we have to delete from fronted list. from backend already deleted.

let result = {};
let list = {};

if (op !== OPERATIONS.DELETE) {
list = await this.getMonitorJSONList(socket.userID, monitorID);
}

result["op"] = op;
result["monitorID"] = monitorID;
result["list"] = list;
this.io.to(socket.userID).emit("monitorList", result);
return result;
}

/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @param {number} monitorID - The ID of monitor for.
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async getMonitorJSONList(userID) {
async getMonitorJSONList(userID, monitorID = null) {
let result = {};
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved

let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
// Initialize query and parameters
vishalsabhaya marked this conversation as resolved.
Show resolved Hide resolved
let query = " user_id = ? ";
let queryParams = [ userID ];

for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
// Add condition for monitorID if provided
if (monitorID) {
query += "AND id = ? ";
queryParams.push(monitorID);
}

let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);

// 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;
}

Expand Down Expand Up @@ -520,3 +558,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");
Loading
Loading