diff --git a/controllers/scheduleController.js b/controllers/scheduleController.js index 0f45d899cbe545b45cfe2e003a9e7ba707041c1b..0855a84620a9c725abb490437de0f1a88b0d16e9 100644 --- a/controllers/scheduleController.js +++ b/controllers/scheduleController.js @@ -1,6 +1,7 @@ // controllers/scheduleController.js const ScheduleService = require('../services/scheduleService'); const ScheduleRequestDTO = require('../dtos/ScheduleRequestDTO'); +const performanceMonitor = require('../utils/performanceMonitor'); class scheduleController { /** @@ -21,20 +22,20 @@ class scheduleController { */ async createSchedule(req, res) { try { - const userId = req.user.id; - const scheduleRequestDTO = new ScheduleRequestDTO(req.body); - const validatedData = scheduleRequestDTO.validate('create'); + return await performanceMonitor.measureAsync('createSchedule', async () => { + const userId = req.user.id; + const scheduleRequestDTO = new ScheduleRequestDTO(req.body); + const validatedData = scheduleRequestDTO.validate('create'); - const schedule = await ScheduleService.createSchedules({ - userId, - ...validatedData - }); + const schedule = await ScheduleService.createSchedules({ + userId, + ...validatedData + }); - return res.status(201).json({ - success: true, - data: { - schedule - } + return res.status(201).json({ + success: true, + data: { schedule } + }); }); } catch (error) { return res.status(400).json({ @@ -61,17 +62,17 @@ class scheduleController { */ async updateSchedules(req, res) { try { - const userId = req.user.id; - const scheduleRequestDTO = new ScheduleRequestDTO(req.body); - const validatedData = scheduleRequestDTO.validate('bulk_update'); + return await performanceMonitor.measureAsync('updateSchedules', async () => { + const userId = req.user.id; + const scheduleRequestDTO = new ScheduleRequestDTO(req.body); + const validatedData = scheduleRequestDTO.validate('bulk_update'); - const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData); - - return res.status(200).json({ - success: true, - data: { - schedule: updatedSchedule - } + const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData); + + return res.status(200).json({ + success: true, + data: { schedule: updatedSchedule } + }); }); } catch (error) { if (error.message === 'Schedule not found') { @@ -104,17 +105,20 @@ class scheduleController { */ async deleteSchedules(req, res) { try { - const userId = req.user.id; - const scheduleRequestDTO = new ScheduleRequestDTO(req.body); - const validatedData = scheduleRequestDTO.validate('bulk_delete'); - const result = await ScheduleService.deleteSchedules(userId, validatedData.title); + return await performanceMonitor.measureAsync('deleteSchedules', async () => { + const userId = req.user.id; + const scheduleRequestDTO = new ScheduleRequestDTO(req.body); + const validatedData = scheduleRequestDTO.validate('bulk_delete'); - return res.status(200).json({ - success: true, - data: { - message: 'Schedule successfully deleted', - deletedCount: result.deletedCount - } + const result = await ScheduleService.deleteSchedules(userId, validatedData.title); + + return res.status(200).json({ + success: true, + data: { + message: 'Schedule successfully deleted', + deletedCount: result.deletedCount + } + }); }); } catch (error) { return res.status(404).json({ @@ -132,14 +136,14 @@ class scheduleController { */ async getAllSchedules(req, res) { try { - const userId = req.user.id; - const schedules = await ScheduleService.getAllSchedules(userId); + return await performanceMonitor.measureAsync('getAllSchedules', async () => { + const userId = req.user.id; + const schedules = await ScheduleService.getAllSchedules(userId); - return res.status(200).json({ - success: true, - data: { - schedules - } + return res.status(200).json({ + success: true, + data: { schedules } + }); }); } catch (error) { return res.status(500).json({ @@ -159,18 +163,18 @@ class scheduleController { */ async getScheduleByTimeIdx(req, res) { try { - const { time_idx } = req.params; - const userId = req.user.id; + return await performanceMonitor.measureAsync('getScheduleByTimeIdx', async () => { + const { time_idx } = req.params; + const userId = req.user.id; + const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) }); + const validatedData = scheduleRequestDTO.validate('get_by_time_idx'); - const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) }); - const validatedData = scheduleRequestDTO.validate('get_by_time_idx'); - const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.time_idx); + const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.time_idx); - return res.status(200).json({ - success: true, - data: { - schedule - } + return res.status(200).json({ + success: true, + data: { schedule } + }); }); } catch (error) { if (error.message === 'Schedule not found') { diff --git a/routes/performanceRoute.js b/routes/performanceRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..eeab98cf437121ab8bd3c8904bbc0683e36a8198 --- /dev/null +++ b/routes/performanceRoute.js @@ -0,0 +1,14 @@ +// routes/performanceRoute.js +const express = require('express'); +const router = express.Router(); +const performanceMonitor = require('../utils/performanceMonitor'); + +router.get('/stats', (req, res) => { + const stats = performanceMonitor.getAllStats(); + res.json({ + success: true, + data: { stats } + }); +}); + +module.exports = router; diff --git a/services/performance.test.js b/services/performance.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e987e86bb2471becaa10ec51713bdc6a1d4c88d2 --- /dev/null +++ b/services/performance.test.js @@ -0,0 +1,203 @@ +// services/performance.test.js +require('dotenv').config(); +const { Op } = require('sequelize'); +const ScheduleService = require('./scheduleService'); +const sequelize = require('../config/sequelize'); +const Schedule = require('../models/schedule'); + +class PerformanceTester { + constructor() { + this.testUserIds = [1, 2, 3, 4, 5]; // 5紐낆쓽 �뚯뒪�� �좎�留� �ъ슜 + this.results = { + operations: { + createSchedules: [], + getAllSchedules: [], + updateSchedules: [], + deleteSchedules: [] + }, + summary: {} + }; + } + + async setup() { + try { + await sequelize.authenticate(); + console.log('Database connection established successfully.'); + await Schedule.destroy({ where: {}, force: true }); + console.log('Test data cleaned successfully.'); + console.log('Using existing user IDs:', this.testUserIds); + } catch (error) { + console.error('Setup failed:', error); + throw error; + } + } + + async runLoadTest() { + console.log('Starting simplified test...'); + + const testSchedules = this.testUserIds.map((userId, i) => ({ + userId, + title: `Test Schedule ${i}`, + is_fixed: true, + time_indices: [i * 2, i * 2 + 1] + })); + + console.log('Test schedules:', testSchedules); + + const transaction = await sequelize.transaction(); + try { + // Create �뚯뒪�� + console.log('\nTesting createSchedules...'); + const createdSchedules = []; + for (const schedule of testSchedules) { + const result = await this.measureOperation('createSchedules', async () => { + const created = await ScheduleService.createSchedules(schedule, transaction); + console.log(`Created schedule for user ${schedule.userId}`); + return created; + }); + if (result) createdSchedules.push(result); + } + await transaction.commit(); + + // �앹꽦�� �ㅼ�以� �뺤씤 + const verifySchedules = await Schedule.findAll({ + where: { + user_id: { [Op.in]: this.testUserIds } + }, + raw: true + }); + console.log('\nVerified schedules:', verifySchedules); + + // GetAll �뚯뒪�� + console.log('\nTesting getAllSchedules...'); + for (const userId of this.testUserIds) { + await this.measureOperation('getAllSchedules', async () => { + return await ScheduleService.getAllSchedules(userId); + }); + } + + // Update �뚯뒪�� + console.log('\nTesting updateSchedules...'); + for (const schedule of createdSchedules) { + await this.measureOperation('updateSchedules', async () => { + return await ScheduleService.updateSchedules(schedule.user_id, { + originalTitle: schedule.title, + title: `Updated ${schedule.title}`, + is_fixed: schedule.is_fixed, + time_indices: schedule.time_indices + }); + }); + } + + // Delete �뚯뒪�� + console.log('\nTesting deleteSchedules...'); + const deleteTransaction = await sequelize.transaction(); + try { + for (const schedule of createdSchedules) { + await this.measureOperation('deleteSchedules', async () => { + return await ScheduleService.deleteSchedules( + schedule.user_id, + `Updated ${schedule.title}`, + deleteTransaction + ); + }); + } + await deleteTransaction.commit(); + } catch (error) { + await deleteTransaction.rollback(); + throw error; + } + } catch (error) { + await transaction.rollback(); + throw error; + } + + this.analyzePerfResults(); + } + + async measureOperation(name, operation) { + const start = process.hrtime.bigint(); + try { + const result = await operation(); + const end = process.hrtime.bigint(); + const duration = Number(end - start) / 1000000; + this.results.operations[name].push({ success: true, duration }); + return result; + } catch (error) { + const end = process.hrtime.bigint(); + const duration = Number(end - start) / 1000000; + this.results.operations[name].push({ + success: false, + duration, + error: error.message + }); + console.error(`Error in ${name}:`, error.message); + return null; + } + } + + analyzePerfResults() { + Object.entries(this.results.operations).forEach(([operation, results]) => { + const successful = results.filter(r => r.success); + const failed = results.filter(r => !r.success); + if (successful.length > 0) { + const durations = successful.map(r => r.duration); + this.results.summary[operation] = { + totalRequests: results.length, + successCount: successful.length, + failCount: failed.length, + avgDuration: durations.reduce((a, b) => a + b, 0) / successful.length, + minDuration: Math.min(...durations), + maxDuration: Math.max(...durations), + p95: this.calculatePercentile(durations, 95), + p99: this.calculatePercentile(durations, 99) + }; + } + }); + this.printResults(); + } + + calculatePercentile(array, percentile) { + const sorted = array.sort((a, b) => a - b); + const index = Math.ceil((percentile / 100) * sorted.length) - 1; + return sorted[index]; + } + + printResults() { + console.log('\n=== Performance Test Results ==='); + Object.entries(this.results.summary).forEach(([operation, stats]) => { + console.log(`\n${operation}:`); + console.log(`Total Requests: ${stats.totalRequests}`); + console.log(`Success Rate: ${((stats.successCount / stats.totalRequests) * 100).toFixed(2)}%`); + console.log(`Average Duration: ${stats.avgDuration.toFixed(2)}ms`); + console.log(`Min Duration: ${stats.minDuration.toFixed(2)}ms`); + console.log(`Max Duration: ${stats.maxDuration.toFixed(2)}ms`); + console.log(`95th Percentile: ${stats.p95.toFixed(2)}ms`); + console.log(`99th Percentile: ${stats.p99.toFixed(2)}ms`); + }); + } + + async cleanup() { + try { + await Schedule.destroy({ where: {}, force: true }); + console.log('Cleanup completed successfully.'); + } catch (error) { + console.error('Cleanup failed:', error); + } + } +} + +async function runTests() { + const tester = new PerformanceTester(); + try { + await tester.setup(); + console.log('Starting performance tests...'); + await tester.runLoadTest(); + } catch (error) { + console.error('Test failed:', error); + } finally { + await sequelize.close(); + } +} + +runTests(); \ No newline at end of file diff --git a/services/scheduleService.js b/services/scheduleService.js index abfb66ef6230eb7e7e24bc811ddf733d3fdacf84..6c66d7ff0398f922ddec76baccc00c2d89fb2a40 100644 --- a/services/scheduleService.js +++ b/services/scheduleService.js @@ -10,34 +10,46 @@ class ScheduleService { * @param {object} [transaction] - Sequelize �몃옖��뀡 媛앹껜 -> 誘명똿諛⑹뿉�� �곌린�꾪빐 �몃옖��뀡�� �섍꺼諛쏅뒗嫄� 異붽� */ async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) { - // 以묐났 寃��� - for (const time_idx of time_indices) { - const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction); - if (overlap) { - throw new Error(`Schedule overlaps at time_idx ${time_idx}`); - } + const overlaps = await Schedule.findAll({ + where: { + user_id: userId, + time_idx: { + [Op.in]: time_indices + } + }, + transaction + }); + + if (overlaps.length > 0) { + throw new Error(`Schedule overlaps at time_idx ${overlaps[0].time_idx}`); } - const createdSchedules = await Promise.all( - time_indices.map(time_idx => - Schedule.create({ - user_id: userId, - title, - time_idx, - is_fixed - }, { transaction }) - ) - ); - - return { - id: createdSchedules[0].id, + const scheduleData = time_indices.map(time_idx => ({ user_id: userId, title, - is_fixed, - time_indices, - createdAt: createdSchedules[0].createdAt, - updatedAt: createdSchedules[0].updatedAt - }; + time_idx, + is_fixed + })); + + try { + const createdSchedules = await Schedule.bulkCreate(scheduleData, { + transaction, + returning: true, + validate: true + }); + + return { + id: createdSchedules[0].id, + user_id: userId, + title, + is_fixed, + time_indices, + createdAt: createdSchedules[0].createdAt, + updatedAt: createdSchedules[0].updatedAt + }; + } catch (error) { + throw new Error(`Failed to bulk create schedules: ${error.message}`); + } } async getAllSchedules(userId) { @@ -55,80 +67,100 @@ class ScheduleService { async updateSchedules(userId, updates, transaction = null) { const { originalTitle, title, is_fixed, time_indices } = updates; - - // 湲곗〈 �ㅼ�以� 議고쉶 - const existingSchedules = await Schedule.findAll({ - where: { - user_id: userId, - title: originalTitle - }, - transaction - }); - - if (existingSchedules.length === 0) { - throw new Error('Schedule not found'); - } - - const existingTimeIndices = existingSchedules.map(s => s.time_idx); // 湲곗〈 �쒓컙�� - const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx)); // ��젣�� �쒓컙�� - const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx)); // 異붽��� �쒓컙�� const t = transaction || await sequelize.transaction(); try { - // ��젣 - if (toDelete.length > 0) { - await Schedule.destroy({ + // 湲곗〈 �ㅼ�以� 議고쉶 + const [existingSchedule, existingSchedules] = await Promise.all([ + Schedule.findOne({ where: { user_id: userId, - title: originalTitle, - time_idx: { - [Op.in]: toDelete - } + title: originalTitle }, transaction: t - }); - } - - // �쒕ぉ, 怨좎젙/�좊룞 �낅뜲�댄듃 - await Schedule.update( - { - title, - is_fixed - }, - { + }), + Schedule.findAll({ + attributes: ['time_idx'], where: { user_id: userId, title: originalTitle }, transaction: t - } + }) + ]); + + if (!existingSchedule) { + throw new Error('Schedule not found'); + } + + const existingTimeIndices = existingSchedules.map(s => s.time_idx); + const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx)); + const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx)); + + // 踰뚰겕 �곗궛 + const operations = []; + + // ��젣 �곗궛 + if (toDelete.length > 0) { + operations.push( + Schedule.destroy({ + where: { + user_id: userId, + title: originalTitle, + time_idx: { + [Op.in]: toDelete + } + }, + transaction: t + }) + ); + } + + // �낅뜲�댄듃 �곗궛 + operations.push( + Schedule.update( + { title, is_fixed }, + { + where: { + user_id: userId, + title: originalTitle + }, + transaction: t + } + ) ); - // �덈줈�� time_indices 異붽� + // �앹꽦 �곗궛 if (toAdd.length > 0) { - await Promise.all( - toAdd.map(time_idx => - Schedule.create({ + operations.push( + Schedule.bulkCreate( + toAdd.map(time_idx => ({ user_id: userId, title, time_idx, is_fixed - }, { transaction: t }) + })), + { + transaction: t, + validate: true + } ) ); } + await Promise.all(operations); // 蹂묐젹 泥섎━ + if (!transaction) { await t.commit(); } return { - id: existingSchedules[0].id, + id: existingSchedule.id, user_id: userId, title, is_fixed, time_indices, - createdAt: existingSchedules[0].createdAt, + createdAt: existingSchedule.createdAt, updatedAt: new Date() }; @@ -157,25 +189,19 @@ class ScheduleService { */ async getScheduleByTimeIdx(userId, time_idx) { // �대떦 time_idx�� �ㅼ�以� 李얘린 - const schedule = await Schedule.findOne({ - where: { user_id: userId, time_idx } - }); - - if (!schedule) { - throw new Error('Schedule not found'); - } - - // 媛숈� �쒕ぉ�� 紐⑤뱺 �ㅼ�以� 李얘린 - const relatedSchedules = await Schedule.findAll({ + const schedules = await Schedule.findAll({ where: { user_id: userId, - title: schedule.title, - is_fixed: schedule.is_fixed + title: { + [Op.in]: sequelize.literal( + `(SELECT title FROM Schedules WHERE user_id = ${userId} AND time_idx = ${time_idx})` + ) + } }, order: [['time_idx', 'ASC']] }); - return ScheduleResponseDTO.groupSchedules(relatedSchedules)[0]; + return ScheduleResponseDTO.groupSchedules(schedules)[0]; } async getAllSchedules(userId) { diff --git a/utils/performanceMonitor.js b/utils/performanceMonitor.js new file mode 100644 index 0000000000000000000000000000000000000000..dfba98533a396a4eaa540ef297c68b21526bdc51 --- /dev/null +++ b/utils/performanceMonitor.js @@ -0,0 +1,63 @@ +// utils/performanceMonitor.js +const { performance, PerformanceObserver } = require('perf_hooks'); + +class PerformanceMonitor { + constructor() { + this.measurements = new Map(); + + // �깅뒫 愿�李곗옄 �ㅼ젙 + this.observer = new PerformanceObserver((items) => { + items.getEntries().forEach((entry) => { + const measurements = this.measurements.get(entry.name) || []; + measurements.push(entry.duration); + this.measurements.set(entry.name, measurements); + + console.log(`Performance Measurement - ${entry.name}: ${entry.duration}ms`); + }); + }); + + this.observer.observe({ entryTypes: ['measure'] }); + } + + async measureAsync(name, fn) { + const start = performance.now(); + try { + return await fn(); + } finally { + const duration = performance.now() - start; + performance.measure(name, { + start, + duration, + detail: { timestamp: new Date().toISOString() } + }); + } + } + + getStats(name) { + const measurements = this.measurements.get(name) || []; + if (measurements.length === 0) return null; + + const sum = measurements.reduce((a, b) => a + b, 0); + const avg = sum / measurements.length; + const min = Math.min(...measurements); + const max = Math.max(...measurements); + + return { + count: measurements.length, + average: avg, + min: min, + max: max, + total: sum + }; + } + + getAllStats() { + const stats = {}; + for (const [name, measurements] of this.measurements.entries()) { + stats[name] = this.getStats(name); + } + return stats; + } +} + +module.exports = new PerformanceMonitor(); \ No newline at end of file