feat(www): make statistics faster

This commit is contained in:
dvelo 2025-03-08 21:10:00 -06:00
parent b36815f5f6
commit 35a0f6dca9
5 changed files with 301 additions and 148 deletions

@ -28,35 +28,66 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { MongoClient } from "mongodb"; import type { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest, NextApiResponse } from "next"; import { MongoClient as MongoClientImpl } from "mongodb";
interface DailyAverage {
day: string;
result: number;
}
interface ResponseData {
result: DailyAverage[];
}
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse<ResponseData | { message: string }>
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClientImpl(process.env.MONGO_DB as string);
try {
const db = client.db("mhsf").collection("history"); const db = client.db("mhsf").collection("history");
const server = req.query.server as string; const server = req.query.server as string;
const daysOfWeek = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] // Use MongoDB aggregation pipeline for better performance
const result = await Promise.all([1,2,3,4,5,6,7].map(async (c) => { const dailyAverages = (await db
const results = await db.find({ .aggregate([
$and: [ {
{ server }, $match: { server },
{ $expr: { $eq: [{ $dayOfWeek: "$date" }, c] } } },
] {
}).toArray() $group: {
_id: { $dayOfWeek: "$date" },
averagePlayerCount: { $avg: "$player_count" },
},
},
{
$project: {
_id: 0,
day: { $arrayElemAt: [daysOfWeek, { $subtract: ["$_id", 1] }] },
result: { $floor: "$averagePlayerCount" },
},
},
{
$sort: { _id: 1 },
},
])
.toArray()) as DailyAverage[];
if (results.length !== 0) { res.send({ result: dailyAverages });
const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count) } catch (error) {
const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length; res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
return { day: daysOfWeek[c - 1], result: Math.floor(average) }; await client.close();
} }
return undefined;
}));
client.close()
res.send({result: result.filter((c) => c !== undefined)});
} }

@ -28,35 +28,64 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { MongoClient } from "mongodb"; import type { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest, NextApiResponse } from "next"; import { MongoClient as MongoClientImpl } from "mongodb";
// Define types for our data
interface ServerHistoricalRecord {
server: string;
[key: string]: unknown;
}
interface ResponseData {
data: Record<string, unknown>[];
}
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse<ResponseData | { message: string }>
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClientImpl(process.env.MONGO_DB as string);
try {
const db = client.db("mhsf").collection("historical"); const db = client.db("mhsf").collection("historical");
const server = req.query.server as string; const server = req.query.server as string;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes); const scopes: string[] = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray(); // Only fetch the fields we need using projection
const data: any[] = []; const projection: Record<string, 1> = { server: 1 };
for (const scope of scopes) {
projection[scope] = 1;
}
allData.forEach((d) => { const allData = await db
const result: any = {}; .find<ServerHistoricalRecord>({ server }, { projection })
scopes.forEach((b) => { .toArray();
result[b] = d[b];
}); // Use map instead of forEach for better performance
data.push(result); const data = allData.map((d) => {
const result: Record<string, unknown> = {};
for (const scope of scopes) {
result[scope] = d[scope];
}
return result;
}); });
client.close();
res.send({ data }); res.send({ data });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
} }
function checkForInfoOrLeave(res: NextApiResponse, info: any) { function checkForInfoOrLeave(
if (info == undefined) res: NextApiResponse,
info: string[] | undefined
): string[] {
if (info === undefined) {
res.status(400).json({ message: "Information wasn't supplied" }); res.status(400).json({ message: "Information wasn't supplied" });
return [];
}
return info; return info;
} }

@ -28,35 +28,78 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { MongoClient } from "mongodb"; import type { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest, NextApiResponse } from "next"; import { MongoClient as MongoClientImpl } from "mongodb";
interface MonthlyAverage {
month: string;
result: number;
}
interface ResponseData {
result: MonthlyAverage[];
}
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse<ResponseData | { message: string }>
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClientImpl(process.env.MONGO_DB as string);
try {
const db = client.db("mhsf").collection("history"); const db = client.db("mhsf").collection("history");
const server = req.query.server as string; const server = req.query.server as string;
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const currentYear = new Date().getFullYear();
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] // Use MongoDB aggregation pipeline for better performance
const result = await Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(async (c) => { const monthlyAverages = (await db
const results = await db.find({ .aggregate([
$and: [ {
{ server }, $match: {
{ date: { $gte: new Date(new Date().getFullYear(), c - 1, 1), $lt: new Date(new Date().getFullYear(), c, 1) } } server,
] date: {
}).toArray() $gte: new Date(currentYear, 0, 1),
$lt: new Date(currentYear + 1, 0, 1),
},
},
},
{
$group: {
_id: { $month: "$date" },
averagePlayerCount: { $avg: "$player_count" },
},
},
{
$project: {
_id: 0,
month: { $arrayElemAt: [months, { $subtract: ["$_id", 1] }] },
result: { $floor: "$averagePlayerCount" },
},
},
{
$sort: { _id: 1 },
},
])
.toArray()) as MonthlyAverage[];
if (results.length !== 0) { res.send({ result: monthlyAverages });
const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count) } catch (error) {
const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length; res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
return { month: months[c - 1], result: Math.floor(average) }; await client.close();
} }
return undefined;
}));
client.close()
res.send({result: result.filter((c) => c !== undefined)});
} }

@ -28,42 +28,92 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { MongoClient } from "mongodb"; import type { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest, NextApiResponse } from "next"; import { MongoClient as MongoClientImpl } from "mongodb";
interface ServerHistoryRecord {
server: string;
player_count: number;
date: Date;
}
interface MHRecord {
total_players: number;
date: Date;
}
interface RelativeData {
relativePrecentage: number;
date: Date;
}
interface ResponseData {
data: RelativeData[];
}
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse<ResponseData | { message: string }>
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClientImpl(process.env.MONGO_DB as string);
try {
const db = client.db("mhsf").collection("history"); const db = client.db("mhsf").collection("history");
const mh = client.db("mhsf").collection("mh"); const mh = client.db("mhsf").collection("mh");
const server = req.query.server as string; const server = req.query.server as string;
const allData = await db.find({ server }).toArray(); // Get only the last 30 records with needed fields
const data: any[] = []; const recentData = await db
if (server === "peww") console.log(allData.slice(-30)); .find<ServerHistoryRecord>(
{ server },
for (const d of allData.slice(-30)) { {
const dateOfEntry = new Date(d.date); projection: { player_count: 1, date: 1 },
const result = await mh sort: { date: -1 },
.find({ limit: 30,
date: { }
$gte: new Date(dateOfEntry.getTime() - 1000 * 60 * 60), )
$lt: new Date(dateOfEntry.getTime() + 1000 * 60 * 60),
},
})
.toArray(); .toArray();
if (result.length > 0) { const data: RelativeData[] = [];
const resultedData = result[0];
data.push({ // Process in batches to reduce the number of database queries
relativePrecentage: d.player_count / resultedData.total_players, const batchSize = 5;
for (let i = 0; i < recentData.length; i += batchSize) {
const batch = recentData.slice(i, i + batchSize);
const batchQueries = batch.map(async (d) => {
const dateOfEntry = new Date(d.date);
const hourBefore = new Date(dateOfEntry.getTime() - 1000 * 60 * 60);
const hourAfter = new Date(dateOfEntry.getTime() + 1000 * 60 * 60);
const result = await mh.findOne<MHRecord>(
{
date: {
$gte: hourBefore,
$lt: hourAfter,
},
},
{ projection: { total_players: 1, date: 1 } }
);
if (result) {
return {
relativePrecentage: d.player_count / result.total_players,
date: dateOfEntry, date: dateOfEntry,
}); };
} }
return null;
});
const batchResults = await Promise.all(batchQueries);
data.push(
...batchResults.filter((item): item is RelativeData => item !== null)
);
} }
client.close();
res.send({ data }); res.send({ data });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
} }

@ -1,67 +1,67 @@
/* import type { NextApiRequest, NextApiResponse } from "next";
* MHSF, Minehut Server List import { MongoClient as MongoClientImpl } from "mongodb";
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { MongoClient } from "mongodb"; // Define types for our data
import { NextApiRequest, NextApiResponse } from "next"; interface ServerHistoryRecord {
server: string;
player_count: number;
date: Date;
[key: string]: unknown;
}
interface ResponseData {
data: Record<string, unknown>[];
dataMax: number;
}
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse<ResponseData | { message: string }>
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClientImpl(process.env.MONGO_DB as string);
try {
const db = client.db("mhsf").collection("history"); const db = client.db("mhsf").collection("history");
const server = req.query.server as string; const server = req.query.server as string;
let dataMax = 0; const scopes: string[] = checkForInfoOrLeave(res, req.body.scopes);
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray(); // Run these queries in parallel for better performance
const data: any[] = []; const [allData, maxPlayerData] = await Promise.all([
db.find<ServerHistoryRecord>({ server }).toArray(),
db
.find<ServerHistoryRecord>({ server })
.sort({ player_count: -1 })
.limit(1)
.toArray(),
]);
dataMax = ( const dataMax =
await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray() maxPlayerData.length > 0 ? maxPlayerData[0].player_count : 0;
)[0].player_count;
allData.forEach((d) => { // Use map instead of forEach for better performance
const result: any = {}; const data = allData.map((d) => {
scopes.forEach((b) => { const result: Record<string, unknown> = {};
result[b] = d[b]; for (const scope of scopes) {
}); result[scope] = d[scope];
data.push(result); }
return result;
}); });
client.close();
res.send({ data, dataMax }); res.send({ data, dataMax });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
} }
function checkForInfoOrLeave(res: NextApiResponse, info: any) { function checkForInfoOrLeave(
if (info == undefined) res: NextApiResponse,
info: string[] | undefined
): string[] {
if (info === undefined) {
res.status(400).json({ message: "Information wasn't supplied" }); res.status(400).json({ message: "Information wasn't supplied" });
return [];
}
return info; return info;
} }