From f2256e1785fc146f77f22100de9be025d88206c8 Mon Sep 17 00:00:00 2001 From: SomeRandomDeveloper Date: Tue, 15 Sep 2026 23:05:01 +0200 Subject: [PATCH 1/3] Refactor aggregateDaily to further reduce memory usage Extract logic into functions so more variables can go out of scope and be garbage collected over time. --- src/aggregateDaily.ts | 86 ++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/src/aggregateDaily.ts b/src/aggregateDaily.ts index 51234fa..6ab6bd3 100644 --- a/src/aggregateDaily.ts +++ b/src/aggregateDaily.ts @@ -1,4 +1,4 @@ -import {aggregateSpeedscopeData} from "./repositories/profileRepository.js"; +import {aggregateSpeedscopeData, AggregationResult} from "./repositories/profileRepository.js"; import {AggregatedProfileType} from "../generated/prisma/enums.js"; import {gzipSync} from "node:zlib"; import {prisma} from "./prisma.js"; @@ -8,40 +8,60 @@ import config from "./config/config.js"; const end = new Date(); const start = new Date(end.getTime() - (24 * 60 * 60 * 1000)); // 1 day ago -const aggregatedProfiles: AggregatedProfile[] = await prisma.aggregatedProfile.findMany({ - where: { - startTime: { - gte: start, +const aggregateData = async ( start: Date, end: Date ) => { + const aggregatedProfiles: AggregatedProfile[] = await prisma.aggregatedProfile.findMany({ + where: { + startTime: { + gte: start, + }, + type: AggregatedProfileType.HOURLY, }, - type: AggregatedProfileType.HOURLY, - }, - orderBy: { - startTime: 'asc', - }, -}); + orderBy: { + startTime: 'asc', + }, + }); -if (!aggregatedProfiles || aggregatedProfiles.length === 0) { - console.log('No profiles found in the last day.'); - process.exit(0); -} + if (!aggregatedProfiles || aggregatedProfiles.length === 0) { + console.log('No profiles found in the last day.'); + process.exit(0); + } -const aggregatedData = aggregateSpeedscopeData( - aggregatedProfiles, - `Daily aggregation (${start.toISOString()} to ${end.toISOString()})` -); + const aggregatedData = aggregateSpeedscopeData( + aggregatedProfiles, + `Daily aggregation (${start.toISOString()} to ${end.toISOString()})` + ); -console.log('Converting data to JSON...'); + const profileCount = aggregatedProfiles + .map((p) => p.profileCount) + .reduce((a, b) => a + b, 0); -const profileJson = JSON.stringify(aggregatedData.file); -delete aggregatedData.file; + return { aggregatedData: aggregatedData, profileCount: profileCount }; +}; -const frameTimingJson = JSON.stringify(aggregatedData.frameTimings, (k, v) => { - if (v instanceof Map) { - return Array.from(v.entries()); - } - return v; -}); -delete aggregatedData.frameTimings; +const compressFrameTimings = ( aggregatedData: AggregationResult ) => { + console.log('Converting frame timings to JSON...'); + const frameTimingJson = JSON.stringify(aggregatedData.frameTimings, (k, v) => { + if (v instanceof Map) { + return Array.from(v.entries()); + } + return v; + }); + delete aggregatedData.frameTimings; + console.log('Compressing frame timings...'); + return gzipSync(frameTimingJson); +}; + +const compressProfile = ( aggregatedData: AggregationResult ) => { + console.log('Converting profile to JSON...'); + const profileJson = JSON.stringify(aggregatedData.file); + delete aggregatedData.file; + console.log('Compressing profile...'); + return gzipSync(profileJson); +}; + +const { aggregatedData, profileCount } = await aggregateData( start, end ); +const compressedProfile = compressProfile( aggregatedData ); +const compressedFrameTimings = compressFrameTimings( aggregatedData ); console.log('Writing data to DB...'); @@ -50,11 +70,9 @@ await prisma.aggregatedProfile.create({ startTime: start, endTime: end, type: AggregatedProfileType.DAILY, - profileCount: aggregatedProfiles - .map((p) => p.profileCount) - .reduce((a, b) => a + b, 0), - speedscopeData: gzipSync(profileJson), - frameTimingData: gzipSync(frameTimingJson), + profileCount: profileCount, + speedscopeData: compressedProfile, + frameTimingData: compressedFrameTimings, } }); From b63281226f1839a6afa5733df0045ce749c30289 Mon Sep 17 00:00:00 2001 From: SomeRandomDeveloper Date: Wed, 16 Sep 2026 00:09:59 +0200 Subject: [PATCH 2/3] Use json-ext and stream JSON chunks to a CompressionStream --- package-lock.json | 10 ++++++++ package.json | 1 + src/aggregateDaily.ts | 37 +++++++++------------------ src/repositories/profileRepository.ts | 6 +++-- src/utils/jsonHelper.ts | 34 ++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 src/utils/jsonHelper.ts diff --git a/package-lock.json b/package-lock.json index 3ca26a9..d2188ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@discoveryjs/json-ext": "^1.1.0", "@prisma/adapter-better-sqlite3": "^7.10.0", "@prisma/client": "^7.10.0", "@types/escape-html": "^1.0.4", @@ -32,6 +33,15 @@ "typescript-eslint": "^8.68.0" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", + "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, "node_modules/@electric-sql/pglite": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", diff --git a/package.json b/package.json index b229641..ce00ce6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "author": "", "license": "ISC", "dependencies": { + "@discoveryjs/json-ext": "^1.1.0", "@prisma/adapter-better-sqlite3": "^7.10.0", "@prisma/client": "^7.10.0", "@types/escape-html": "^1.0.4", diff --git a/src/aggregateDaily.ts b/src/aggregateDaily.ts index 6ab6bd3..ab0ef42 100644 --- a/src/aggregateDaily.ts +++ b/src/aggregateDaily.ts @@ -1,9 +1,13 @@ -import {aggregateSpeedscopeData, AggregationResult} from "./repositories/profileRepository.js"; +import { + aggregateSpeedscopeData, + FrameTimings +} from "./repositories/profileRepository.js"; import {AggregatedProfileType} from "../generated/prisma/enums.js"; -import {gzipSync} from "node:zlib"; import {prisma} from "./prisma.js"; import {AggregatedProfile} from "../generated/prisma/client.js"; import config from "./config/config.js"; +import {jsonifyAndCompressFrameTimings, jsonifyAndCompressProfile} from "./utils/jsonHelper.js"; +import {SpeedscopeFile} from "./models/speedscope"; const end = new Date(); const start = new Date(end.getTime() - (24 * 60 * 60 * 1000)); // 1 day ago @@ -38,30 +42,13 @@ const aggregateData = async ( start: Date, end: Date ) => { return { aggregatedData: aggregatedData, profileCount: profileCount }; }; -const compressFrameTimings = ( aggregatedData: AggregationResult ) => { - console.log('Converting frame timings to JSON...'); - const frameTimingJson = JSON.stringify(aggregatedData.frameTimings, (k, v) => { - if (v instanceof Map) { - return Array.from(v.entries()); - } - return v; - }); - delete aggregatedData.frameTimings; - console.log('Compressing frame timings...'); - return gzipSync(frameTimingJson); -}; - -const compressProfile = ( aggregatedData: AggregationResult ) => { - console.log('Converting profile to JSON...'); - const profileJson = JSON.stringify(aggregatedData.file); - delete aggregatedData.file; - console.log('Compressing profile...'); - return gzipSync(profileJson); -}; - const { aggregatedData, profileCount } = await aggregateData( start, end ); -const compressedProfile = compressProfile( aggregatedData ); -const compressedFrameTimings = compressFrameTimings( aggregatedData ); +console.log('Compressing profile...'); +const compressedProfile = await jsonifyAndCompressProfile( aggregatedData.file as SpeedscopeFile ); +delete aggregatedData.file; +console.log('Compressing frame timings...'); +const compressedFrameTimings = await jsonifyAndCompressFrameTimings( aggregatedData.frameTimings as FrameTimings ); +delete aggregatedData.frameTimings; console.log('Writing data to DB...'); diff --git a/src/repositories/profileRepository.ts b/src/repositories/profileRepository.ts index c51bf22..638387d 100644 --- a/src/repositories/profileRepository.ts +++ b/src/repositories/profileRepository.ts @@ -5,9 +5,11 @@ import {AggregatedProfile, Profile} from "../../generated/prisma/client"; import {gunzipSync} from "node:zlib"; import config from "../config/config.js"; +export type FrameTimings = Map>; + export interface AggregationResult { file?: SpeedscopeFile; - frameTimings?: Map>; + frameTimings?: FrameTimings; } /** @@ -30,7 +32,7 @@ export function aggregateSpeedscopeData(data: (Profile|AggregatedProfile)[], nam } const globalFrames = new Map(); - const frameTimings: Map> = new Map(); + const frameTimings: FrameTimings = new Map(); const globalFramesRev: SpeedscopeFrame[] = []; const globalSamples = new Map(); let json: SpeedscopeFile | undefined; diff --git a/src/utils/jsonHelper.ts b/src/utils/jsonHelper.ts new file mode 100644 index 0000000..f960d3b --- /dev/null +++ b/src/utils/jsonHelper.ts @@ -0,0 +1,34 @@ +import {SpeedscopeFile} from "../models/speedscope"; +import {pipeline} from "node:stream/promises"; +import {Replacer, stringifyChunked} from "@discoveryjs/json-ext"; +import {arrayBuffer} from "node:stream/consumers"; +import {FrameTimings} from "../repositories/profileRepository"; + +const jsonifyAndCompress = async ( + obj: unknown, + replacer?: Replacer +): Promise> => { + const compressionStream = new CompressionStream("gzip"); + const [buffer] = await Promise.all([ + arrayBuffer(compressionStream.readable), + pipeline(stringifyChunked(obj, replacer), compressionStream.writable), + ]); + return Buffer.from(buffer); +}; + +export const jsonifyAndCompressProfile = async ( + profile: SpeedscopeFile +): Promise> => { + return jsonifyAndCompress(profile); +}; + +export const jsonifyAndCompressFrameTimings = async ( + frameTimings: FrameTimings +): Promise> => { + return jsonifyAndCompress(frameTimings, (k, v) => { + if (v instanceof Map) { + return Array.from(v.entries()); + } + return v; + }); +}; From ddeb00ed4ddab33b7c32980cc963facdd5e885bd Mon Sep 17 00:00:00 2001 From: SomeRandomDeveloper Date: Wed, 16 Sep 2026 00:36:53 +0200 Subject: [PATCH 3/3] Use stylistic to enforce code style in CI Also autofix existing issues --- eslint.config.mjs | 25 +++- package-lock.json | 70 ++++++++--- package.json | 4 +- src/aggregateDaily.ts | 32 ++--- src/aggregateHourly.ts | 16 +-- src/app.ts | 4 +- src/controllers/profileController.ts | 92 +++++++------- src/middlewares/authentication.ts | 12 +- src/middlewares/errorHandler.ts | 2 +- src/prisma.ts | 6 +- src/repositories/profileRepository.ts | 8 +- src/routes/routes.ts | 166 +++++++++++++------------- src/utils/jsonHelper.ts | 20 ++-- 13 files changed, 257 insertions(+), 200 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 7f97268..e61b501 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,8 +3,29 @@ import eslint from '@eslint/js'; import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; +import stylistic from '@stylistic/eslint-plugin'; export default defineConfig( - eslint.configs.recommended, - tseslint.configs.recommended, + eslint.configs.recommended, + tseslint.configs.recommended, + stylistic.configs.recommended, + { + rules: { + '@stylistic/brace-style': ['error', '1tbs'], + '@stylistic/member-delimiter-style': [ + 'error', + { + multiline: { + delimiter: 'semi', + requireLast: true, + }, + singleline: { + delimiter: 'comma', + requireLast: false, + }, + }, + ], + '@stylistic/semi': ['error', 'always'], + }, + }, ); diff --git a/package-lock.json b/package-lock.json index d2188ad..33bf302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,12 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^26.1.2", "eslint": "^10.8.0", - "prettier": "^3.9.6", "prisma": "^7.9.1", "tsx": "^4.22.4", "typescript": "^5.9.3", @@ -1108,6 +1108,58 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -3847,22 +3899,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prisma": { "version": "7.9.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz", diff --git a/package.json b/package.json index ce00ce6..4dfac64 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "node dist/src/server.js", "dev": "tsx watch src/server.ts", "lint": "eslint 'src/**/*.ts'", - "format": "prettier --write 'src/**/*.ts'" + "format": "eslint 'src/**/*.ts' --fix" }, "keywords": [], "author": "", @@ -26,12 +26,12 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^26.1.2", "eslint": "^10.8.0", - "prettier": "^3.9.6", "prisma": "^7.9.1", "tsx": "^4.22.4", "typescript": "^5.9.3", diff --git a/src/aggregateDaily.ts b/src/aggregateDaily.ts index ab0ef42..c87d282 100644 --- a/src/aggregateDaily.ts +++ b/src/aggregateDaily.ts @@ -1,18 +1,18 @@ import { aggregateSpeedscopeData, - FrameTimings -} from "./repositories/profileRepository.js"; -import {AggregatedProfileType} from "../generated/prisma/enums.js"; -import {prisma} from "./prisma.js"; -import {AggregatedProfile} from "../generated/prisma/client.js"; -import config from "./config/config.js"; -import {jsonifyAndCompressFrameTimings, jsonifyAndCompressProfile} from "./utils/jsonHelper.js"; -import {SpeedscopeFile} from "./models/speedscope"; + FrameTimings, +} from './repositories/profileRepository.js'; +import { AggregatedProfileType } from '../generated/prisma/enums.js'; +import { prisma } from './prisma.js'; +import { AggregatedProfile } from '../generated/prisma/client.js'; +import config from './config/config.js'; +import { jsonifyAndCompressFrameTimings, jsonifyAndCompressProfile } from './utils/jsonHelper.js'; +import { SpeedscopeFile } from './models/speedscope'; const end = new Date(); const start = new Date(end.getTime() - (24 * 60 * 60 * 1000)); // 1 day ago -const aggregateData = async ( start: Date, end: Date ) => { +const aggregateData = async (start: Date, end: Date) => { const aggregatedProfiles: AggregatedProfile[] = await prisma.aggregatedProfile.findMany({ where: { startTime: { @@ -31,23 +31,23 @@ const aggregateData = async ( start: Date, end: Date ) => { } const aggregatedData = aggregateSpeedscopeData( - aggregatedProfiles, - `Daily aggregation (${start.toISOString()} to ${end.toISOString()})` + aggregatedProfiles, + `Daily aggregation (${start.toISOString()} to ${end.toISOString()})`, ); const profileCount = aggregatedProfiles - .map((p) => p.profileCount) + .map(p => p.profileCount) .reduce((a, b) => a + b, 0); return { aggregatedData: aggregatedData, profileCount: profileCount }; }; -const { aggregatedData, profileCount } = await aggregateData( start, end ); +const { aggregatedData, profileCount } = await aggregateData(start, end); console.log('Compressing profile...'); -const compressedProfile = await jsonifyAndCompressProfile( aggregatedData.file as SpeedscopeFile ); +const compressedProfile = await jsonifyAndCompressProfile(aggregatedData.file as SpeedscopeFile); delete aggregatedData.file; console.log('Compressing frame timings...'); -const compressedFrameTimings = await jsonifyAndCompressFrameTimings( aggregatedData.frameTimings as FrameTimings ); +const compressedFrameTimings = await jsonifyAndCompressFrameTimings(aggregatedData.frameTimings as FrameTimings); delete aggregatedData.frameTimings; console.log('Writing data to DB...'); @@ -60,7 +60,7 @@ await prisma.aggregatedProfile.create({ profileCount: profileCount, speedscopeData: compressedProfile, frameTimingData: compressedFrameTimings, - } + }, }); if (config.purgeHourlyAggregations) { diff --git a/src/aggregateHourly.ts b/src/aggregateHourly.ts index 9c41b52..692f829 100644 --- a/src/aggregateHourly.ts +++ b/src/aggregateHourly.ts @@ -1,8 +1,8 @@ -import {aggregateSpeedscopeData} from './repositories/profileRepository.js'; -import {prisma} from "./prisma.js"; -import {gzipSync} from "node:zlib"; -import {AggregatedProfileType, Profile} from "../generated/prisma/client.js"; -import config from "./config/config.js"; +import { aggregateSpeedscopeData } from './repositories/profileRepository.js'; +import { prisma } from './prisma.js'; +import { gzipSync } from 'node:zlib'; +import { AggregatedProfileType, Profile } from '../generated/prisma/client.js'; +import config from './config/config.js'; const end = new Date(); const start = new Date(end.getTime() - (60 * 60 * 1000)); // 1 hour ago @@ -24,8 +24,8 @@ if (profiles.length === 0) { process.exit(0); } const aggregatedData = aggregateSpeedscopeData( - profiles, - `Hourly aggregation (${start.toISOString()} to ${end.toISOString()})` + profiles, + `Hourly aggregation (${start.toISOString()} to ${end.toISOString()})`, ); await prisma.aggregatedProfile.create({ data: { @@ -40,7 +40,7 @@ await prisma.aggregatedProfile.create({ } return v; })), - } + }, }); if (config.purgeProfiles) { diff --git a/src/app.ts b/src/app.ts index 1402cac..c7961b3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,8 @@ import express from 'express'; import routes from './routes/routes.js'; import { errorHandler } from './middlewares/errorHandler.js'; -import path from "node:path"; -import {fileURLToPath} from "node:url"; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; const app = express(); diff --git a/src/controllers/profileController.ts b/src/controllers/profileController.ts index 9bec5c3..639bdb6 100644 --- a/src/controllers/profileController.ts +++ b/src/controllers/profileController.ts @@ -1,8 +1,8 @@ import type { NextFunction, Request, Response } from 'express'; -import type {AggregatedProfileType, Profile} from "../../generated/prisma/client.js"; -import {prisma} from "../prisma.js"; -import {gunzipSync, gzipSync} from "node:zlib"; -import escapeHTML from "escape-html"; +import type { AggregatedProfileType, Profile } from '../../generated/prisma/client.js'; +import { prisma } from '../prisma.js'; +import { gunzipSync, gzipSync } from 'node:zlib'; +import escapeHTML from 'escape-html'; export const logProfile = async ( req: Request, @@ -22,7 +22,7 @@ export const logProfile = async ( } = req.body; const exists = await prisma.profile.findUnique({ - where: { id } + where: { id }, }); if (exists) { return res @@ -60,7 +60,7 @@ export const getProfile = async ( const { id } = req.params; const profile = await prisma.profile.findUnique({ - where: { id: id as string } + where: { id: id as string }, }) as Profile | null; if (!profile) { return res.status(404).json({ error: 'Profile not found' }); @@ -88,33 +88,33 @@ const createViewHtml = (profileUrl: string) => { }; export const viewLatestAggregation = async ( - req: Request, - res: Response, + req: Request, + res: Response, ) => { const { type } = req.params; res.status(200).send(createViewHtml(`/#profileURL=/aggregation/latest/${type}`)); }; export const viewAggregation = async ( - req: Request, - res: Response, + req: Request, + res: Response, ) => { const { id } = req.params; res.status(200).send(createViewHtml(`/#profileURL=/aggregation/${id}`)); }; export const viewProfile = async ( - req: Request, - res: Response, + req: Request, + res: Response, ) => { const { id } = req.params; res.status(200).send(createViewHtml(`/#profileURL=/profile/${id}`)); }; export const getProfileMetadata = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { const { id } = req.params; @@ -130,7 +130,7 @@ export const getProfileMetadata = async ( timestamp: true, environment: true, parserReport: true, - } + }, }) as Profile | null; if (!profile) { return res.status(404).json({ error: 'Profile not found' }); @@ -149,33 +149,33 @@ export const getProfileMetadata = async ( } catch (error) { next(error); } -} +}; export const getAggregations = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { - const {type} = req.params; + const { type } = req.params; - const where = type ? {type: type as AggregatedProfileType} : {}; + const where = type ? { type: type as AggregatedProfileType } : {}; const aggregatedProfiles = await prisma.aggregatedProfile.findMany({ where, - orderBy: {endTime: 'desc'}, + orderBy: { endTime: 'desc' }, select: { id: true, startTime: true, endTime: true, type: true, profileCount: true, - } + }, }); res.status(200).json(aggregatedProfiles); } catch (error) { next(error); } -} +}; export const getLatestAggregation = async ( req: Request, @@ -201,9 +201,9 @@ export const getLatestAggregation = async ( }; export const getLatestAggregationMetadata = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { const { type } = req.params; @@ -217,7 +217,7 @@ export const getLatestAggregationMetadata = async ( endTime: true, type: true, profileCount: true, - } + }, }); if (!aggregatedProfile) { return res.status(404).json({ error: 'No aggregated profiles found' }); @@ -227,12 +227,12 @@ export const getLatestAggregationMetadata = async ( } catch (error) { next(error); } -} +}; export const getLatestFrameTimingData = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { const { type } = req.params; @@ -253,9 +253,9 @@ export const getLatestFrameTimingData = async ( }; export const getAggregationById = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { const { id } = req.params; @@ -272,12 +272,12 @@ export const getAggregationById = async ( } catch (error) { next(error); } -} +}; export const getAggregationMetadataById = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { const { id } = req.params; @@ -290,7 +290,7 @@ export const getAggregationMetadataById = async ( endTime: true, type: true, profileCount: true, - } + }, }); if (!aggregatedProfile) { return res.status(404).json({ error: 'Aggregated profile not found' }); @@ -300,12 +300,12 @@ export const getAggregationMetadataById = async ( } catch (error) { next(error); } -} +}; export const getAggregationFrameTimingDataById = async ( - req: Request, - res: Response, - next: NextFunction, + req: Request, + res: Response, + next: NextFunction, ) => { try { const { id } = req.params; @@ -322,4 +322,4 @@ export const getAggregationFrameTimingDataById = async ( } catch (error) { next(error); } -} +}; diff --git a/src/middlewares/authentication.ts b/src/middlewares/authentication.ts index 380eaa6..7057976 100644 --- a/src/middlewares/authentication.ts +++ b/src/middlewares/authentication.ts @@ -1,11 +1,11 @@ -import {timingSafeEqual} from "node:crypto"; -import config from "../config/config.js"; -import {Request, Response, NextFunction} from "express"; +import { timingSafeEqual } from 'node:crypto'; +import config from '../config/config.js'; +import { Request, Response, NextFunction } from 'express'; function requireAuth( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction, ) { const auth = req.headers.authorization; const expected = `Bearer ${config.logToken}`; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index f610be5..3e47c40 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -11,7 +11,7 @@ export const errorHandler = ( req: Request, res: Response, // eslint-disable-next-line @typescript-eslint/no-unused-vars - next: NextFunction + next: NextFunction, ) => { console.error(err); const showMessage = config.nodeEnv === 'development'; diff --git a/src/prisma.ts b/src/prisma.ts index 5fc8a71..015c1d4 100644 --- a/src/prisma.ts +++ b/src/prisma.ts @@ -1,6 +1,6 @@ -import 'dotenv/config' -import {PrismaBetterSqlite3} from "@prisma/adapter-better-sqlite3"; -import {PrismaClient} from "../generated/prisma/client.js"; +import 'dotenv/config'; +import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; +import { PrismaClient } from '../generated/prisma/client.js'; const connectionString = `${process.env.DATABASE_URL}`; const adapter = new PrismaBetterSqlite3({ url: connectionString }); diff --git a/src/repositories/profileRepository.ts b/src/repositories/profileRepository.ts index 638387d..90fb69d 100644 --- a/src/repositories/profileRepository.ts +++ b/src/repositories/profileRepository.ts @@ -1,9 +1,9 @@ import type { SpeedscopeFile, SpeedscopeFrame, } from '../models/speedscope.js'; -import {AggregatedProfile, Profile} from "../../generated/prisma/client"; -import {gunzipSync} from "node:zlib"; -import config from "../config/config.js"; +import { AggregatedProfile, Profile } from '../../generated/prisma/client'; +import { gunzipSync } from 'node:zlib'; +import config from '../config/config.js'; export type FrameTimings = Map>; @@ -16,7 +16,7 @@ export interface AggregationResult { * @param data Array of profiles to aggregate. Must contain at least one entry. * @param name The name to assign to the aggregated profile. */ -export function aggregateSpeedscopeData(data: (Profile|AggregatedProfile)[], name: string): AggregationResult { +export function aggregateSpeedscopeData(data: (Profile | AggregatedProfile)[], name: string): AggregationResult { if (data.length === 0) { throw new Error('No data to aggregate!'); } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 8bdc744..1f0e61f 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -14,12 +14,12 @@ import { viewLatestAggregation, viewProfile, } from '../controllers/profileController.js'; -import {body, param} from 'express-validator'; +import { body, param } from 'express-validator'; import { handleValidationErrors } from '../middlewares/errorHandler.js'; import cors from 'cors'; -import config from "../config/config.js"; -import {AggregatedProfileType} from "../../generated/prisma/enums.js"; -import requireAuth from "../middlewares/authentication.js"; +import config from '../config/config.js'; +import { AggregatedProfileType } from '../../generated/prisma/enums.js'; +import requireAuth from '../middlewares/authentication.js'; const router = Router(); @@ -31,41 +31,41 @@ router.get( origin: config.allowedOrigin, }), handleValidationErrors, - getProfile + getProfile, ); router.get( - '/view/aggregation/latest/:type', [ - param('type').isString().notEmpty().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]), - ], - handleValidationErrors, - viewLatestAggregation + '/view/aggregation/latest/:type', [ + param('type').isString().notEmpty().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]), + ], + handleValidationErrors, + viewLatestAggregation, ); router.get( - '/view/aggregation/:id', [ - param('id').isInt(), - ], - handleValidationErrors, - viewAggregation + '/view/aggregation/:id', [ + param('id').isInt(), + ], + handleValidationErrors, + viewAggregation, ); router.get( - '/view/:id', [ - param('id').isString().notEmpty(), - ], - handleValidationErrors, - viewProfile -) + '/view/:id', [ + param('id').isString().notEmpty(), + ], + handleValidationErrors, + viewProfile, +); router.get( - '/metadata/:id', - [ - param('id').isString().notEmpty(), - ], - handleValidationErrors, - getProfileMetadata -) + '/metadata/:id', + [ + param('id').isString().notEmpty(), + ], + handleValidationErrors, + getProfileMetadata, +); router.post('/log', requireAuth, express.json({ limit: config.requestSizeLimit }), [ body('id').isString().notEmpty(), @@ -79,80 +79,80 @@ router.post('/log', requireAuth, express.json({ limit: config.requestSizeLimit } ], handleValidationErrors, logProfile); router.get( - '/aggregations', - getAggregations -) + '/aggregations', + getAggregations, +); router.get( - '/aggregations/:type', - [ - param('type').notEmpty().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]) - ], - handleValidationErrors, - getAggregations -) + '/aggregations/:type', + [ + param('type').notEmpty().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]), + ], + handleValidationErrors, + getAggregations, +); router.get( - '/aggregation/latest/:type', - cors({ - origin: config.allowedOrigin, - }), - [ - param('type').exists().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]) - ], - handleValidationErrors, - getLatestAggregation + '/aggregation/latest/:type', + cors({ + origin: config.allowedOrigin, + }), + [ + param('type').exists().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]), + ], + handleValidationErrors, + getLatestAggregation, ); router.get( - '/aggregation/latest/:type/metadata', - [ - param('type').exists().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]) - ], - handleValidationErrors, - getLatestAggregationMetadata + '/aggregation/latest/:type/metadata', + [ + param('type').exists().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]), + ], + handleValidationErrors, + getLatestAggregationMetadata, ); router.get( - '/aggregation/latest/:type/frame-timings', - [ - param('type').exists().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]) - ], - handleValidationErrors, - getLatestFrameTimingData + '/aggregation/latest/:type/frame-timings', + [ + param('type').exists().toUpperCase().isIn([AggregatedProfileType.HOURLY, AggregatedProfileType.DAILY]), + ], + handleValidationErrors, + getLatestFrameTimingData, ); router.get( - '/aggregation/:id', - cors({ - origin: config.allowedOrigin, - }), - [ - param('id').isInt(), - ], - handleValidationErrors, - getAggregationById + '/aggregation/:id', + cors({ + origin: config.allowedOrigin, + }), + [ + param('id').isInt(), + ], + handleValidationErrors, + getAggregationById, ); router.get( - '/aggregation/:id/metadata', - cors({ - origin: config.allowedOrigin, - }), - [ - param('id').isInt(), - ], - handleValidationErrors, - getAggregationMetadataById + '/aggregation/:id/metadata', + cors({ + origin: config.allowedOrigin, + }), + [ + param('id').isInt(), + ], + handleValidationErrors, + getAggregationMetadataById, ); router.get( - '/aggregation/:id/frame-timings', - [ - param('id').isInt(), - ], - handleValidationErrors, - getAggregationFrameTimingDataById + '/aggregation/:id/frame-timings', + [ + param('id').isInt(), + ], + handleValidationErrors, + getAggregationFrameTimingDataById, ); export default router; diff --git a/src/utils/jsonHelper.ts b/src/utils/jsonHelper.ts index f960d3b..31d34a3 100644 --- a/src/utils/jsonHelper.ts +++ b/src/utils/jsonHelper.ts @@ -1,14 +1,14 @@ -import {SpeedscopeFile} from "../models/speedscope"; -import {pipeline} from "node:stream/promises"; -import {Replacer, stringifyChunked} from "@discoveryjs/json-ext"; -import {arrayBuffer} from "node:stream/consumers"; -import {FrameTimings} from "../repositories/profileRepository"; +import { SpeedscopeFile } from '../models/speedscope'; +import { pipeline } from 'node:stream/promises'; +import { Replacer, stringifyChunked } from '@discoveryjs/json-ext'; +import { arrayBuffer } from 'node:stream/consumers'; +import { FrameTimings } from '../repositories/profileRepository'; const jsonifyAndCompress = async ( - obj: unknown, - replacer?: Replacer + obj: unknown, + replacer?: Replacer, ): Promise> => { - const compressionStream = new CompressionStream("gzip"); + const compressionStream = new CompressionStream('gzip'); const [buffer] = await Promise.all([ arrayBuffer(compressionStream.readable), pipeline(stringifyChunked(obj, replacer), compressionStream.writable), @@ -17,13 +17,13 @@ const jsonifyAndCompress = async ( }; export const jsonifyAndCompressProfile = async ( - profile: SpeedscopeFile + profile: SpeedscopeFile, ): Promise> => { return jsonifyAndCompress(profile); }; export const jsonifyAndCompressFrameTimings = async ( - frameTimings: FrameTimings + frameTimings: FrameTimings, ): Promise> => { return jsonifyAndCompress(frameTimings, (k, v) => { if (v instanceof Map) {