Skip to content

Commit

Permalink
improve page load performance of large amount urls (#5025)
Browse files Browse the repository at this point in the history
Co-authored-by: vishal sabhaya <vishals@vebuin.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
  • Loading branch information
3 people authored Oct 6, 2024
1 parent f791d4a commit d0067a0
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 68 deletions.
173 changes: 137 additions & 36 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,31 +72,20 @@ class Monitor extends BeanModel {

/**
* Return an object that ready to parse to JSON
* @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function
* @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {Promise<object>} Object ready to parse
* @returns {object} Object ready to parse
*/
async toJSON(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();
toJSON(preloadData = {}, includeSensitiveData = true) {

let screenshot = null;

if (this.type === "real-browser") {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}

const path = await this.getPath();
const path = preloadData.paths.get(this.id) || [];
const pathName = path.join(" / ");

let data = {
Expand All @@ -106,15 +95,15 @@ class Monitor extends BeanModel {
path,
pathName,
parent: this.parent,
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
childrenIDs: preloadData.childrenIDs.get(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.get(this.id),
forceInactive: preloadData.forceInactive.get(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
Expand All @@ -134,9 +123,9 @@ class Monitor extends BeanModel {
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
notificationIDList: preloadData.notifications.get(this.id) || {},
tags: preloadData.tags.get(this.id) || [],
maintenance: preloadData.maintenanceStatus.get(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType,
Expand Down Expand Up @@ -202,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 @@ -1197,6 +1176,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 @@ -1333,7 +1324,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 @@ -1344,7 +1338,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, monitor.toJSON(preloadData, false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
Expand Down Expand Up @@ -1507,6 +1501,111 @@ 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 notificationsMap = new Map();
const tagsMap = new Map();
const maintenanceStatusMap = new Map();
const childrenIDsMap = new Map();
const activeStatusMap = new Map();
const forceInactiveMap = new Map();
const pathsMap = new Map();

if (monitorData.length > 0) {
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)));
const paths = await Promise.all(monitorData.map(monitor => Monitor.getAllPath(monitor.id, monitor.name)));

notifications.forEach(row => {
if (!notificationsMap.has(row.monitor_id)) {
notificationsMap.set(row.monitor_id, {});
}
notificationsMap.get(row.monitor_id)[row.notification_id] = true;
});

tags.forEach(row => {
if (!tagsMap.has(row.monitor_id)) {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
name: row.name,
color: row.color
});
});

monitorData.forEach((monitor, index) => {
maintenanceStatusMap.set(monitor.id, maintenanceStatuses[index]);
});

monitorData.forEach((monitor, index) => {
childrenIDsMap.set(monitor.id, childrenIDs[index]);
});

monitorData.forEach((monitor, index) => {
activeStatusMap.set(monitor.id, activeStatuses[index]);
});

monitorData.forEach((monitor, index) => {
forceInactiveMap.set(monitor.id, !forceInactiveStatuses[index]);
});

monitorData.forEach((monitor, index) => {
pathsMap.set(monitor.id, paths[index]);
});
}

return {
notifications: notificationsMap,
tags: tagsMap,
maintenanceStatus: maintenanceStatusMap,
childrenIDs: childrenIDsMap,
activeStatus: activeStatusMap,
forceInactive: forceInactiveMap,
paths: pathsMap,
};
}

/**
* Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get
Expand Down Expand Up @@ -1539,16 +1638,18 @@ class Monitor extends BeanModel {

/**
* Gets the full path
* @param {number} monitorID ID of the monitor to get
* @param {string} name of the monitor to get
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
*/
async getPath() {
const path = [ this.name ];
static async getAllPath(monitorID, name) {
const path = [ name ];

if (this.parent === null) {
return path;
}

let parent = await Monitor.getParent(this.id);
let parent = await Monitor.getParent(monitorID);
while (parent !== null) {
path.unshift(parent.name);
parent = await Monitor.getParent(parent.id);
Expand Down
30 changes: 16 additions & 14 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ let needSetup = false;

await updateMonitorNotification(bean.id, notificationIDList);

await server.sendMonitorList(socket);
await server.sendUpdateMonitorIntoList(socket, bean.id);

if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
Expand Down Expand Up @@ -879,11 +879,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.sendUpdateMonitorIntoList(socket, bean.id);

callback({
ok: true,
Expand Down Expand Up @@ -923,14 +923,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: monitor.toJSON(preloadData),
});

} catch (e) {
Expand Down Expand Up @@ -981,7 +984,7 @@ let needSetup = false;
try {
checkLogin(socket);
await startMonitor(socket.userID, monitorID);
await server.sendMonitorList(socket);
await server.sendUpdateMonitorIntoList(socket, monitorID);

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

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

await server.sendMonitorList(socket);
await server.sendDeleteMonitorFromList(socket, monitorID);

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

await StatusPage.sendStatusPageList(io, socket);

const monitorPromises = [];
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
monitorPromises.push(sendHeartbeatList(socket, monitorID));
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
}

for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
await Promise.all(monitorPromises);

// Set server timezone from client browser if not set
// It should be run once only
Expand Down
Loading

0 comments on commit d0067a0

Please sign in to comment.