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.
*/
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import type { 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(
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 server = req.query.server as string;
const daysOfWeek = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const result = await Promise.all([1,2,3,4,5,6,7].map(async (c) => {
const results = await db.find({
$and: [
{ server },
{ $expr: { $eq: [{ $dayOfWeek: "$date" }, c] } }
]
}).toArray()
// Use MongoDB aggregation pipeline for better performance
const dailyAverages = (await db
.aggregate([
{
$match: { server },
},
{
$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) {
const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count)
const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length;
return { day: daysOfWeek[c - 1], result: Math.floor(average) };
res.send({ result: dailyAverages });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
return undefined;
}));
client.close()
res.send({result: result.filter((c) => c !== undefined)});
}

@ -28,35 +28,64 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import type { 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(
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 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();
const data: any[] = [];
// Only fetch the fields we need using projection
const projection: Record<string, 1> = { server: 1 };
for (const scope of scopes) {
projection[scope] = 1;
}
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
result[b] = d[b];
});
data.push(result);
const allData = await db
.find<ServerHistoricalRecord>({ server }, { projection })
.toArray();
// Use map instead of forEach for better performance
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 });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
function checkForInfoOrLeave(
res: NextApiResponse,
info: string[] | undefined
): string[] {
if (info === undefined) {
res.status(400).json({ message: "Information wasn't supplied" });
return [];
}
return info;
}

@ -28,35 +28,78 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import type { 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(
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 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']
const result = await Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(async (c) => {
const results = await db.find({
$and: [
{ server },
{ date: { $gte: new Date(new Date().getFullYear(), c - 1, 1), $lt: new Date(new Date().getFullYear(), c, 1) } }
]
}).toArray()
// Use MongoDB aggregation pipeline for better performance
const monthlyAverages = (await db
.aggregate([
{
$match: {
server,
date: {
$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) {
const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count)
const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length;
return { month: months[c - 1], result: Math.floor(average) };
res.send({ result: monthlyAverages });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
return undefined;
}));
client.close()
res.send({result: result.filter((c) => c !== undefined)});
}

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

@ -1,67 +1,67 @@
/*
* MHSF, Minehut Server List
* 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 type { NextApiRequest, NextApiResponse } from "next";
import { MongoClient as MongoClientImpl } from "mongodb";
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
// Define types for our data
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(
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 server = req.query.server as string;
let dataMax = 0;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const scopes: string[] = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray();
const data: any[] = [];
// Run these queries in parallel for better performance
const [allData, maxPlayerData] = await Promise.all([
db.find<ServerHistoryRecord>({ server }).toArray(),
db
.find<ServerHistoryRecord>({ server })
.sort({ player_count: -1 })
.limit(1)
.toArray(),
]);
dataMax = (
await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray()
)[0].player_count;
const dataMax =
maxPlayerData.length > 0 ? maxPlayerData[0].player_count : 0;
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
result[b] = d[b];
});
data.push(result);
// Use map instead of forEach for better performance
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, dataMax });
} catch (error) {
res.status(500).json({ message: "An error occurred while fetching data" });
} finally {
await client.close();
}
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
function checkForInfoOrLeave(
res: NextApiResponse,
info: string[] | undefined
): string[] {
if (info === undefined) {
res.status(400).json({ message: "Information wasn't supplied" });
return [];
}
return info;
}