Commit b73921eb authored by avelichko's avatar avelichko

Правки

parent 46f27ebd
{
"git.ignoreLimitWarning": true
}
\ No newline at end of file
# Языки написания спецификаций. Задание 2 # Языки написания спецификаций. Задание 2
Автор: **Величко Арсений, ИВТ 4** Автор: **Величко Арсений, ИВТ 4**
[**Ссылка на скринкаст**](https://youtu.be/FP9bpyjhszg)
Тема моей ВКР - "Оптимизация выполнения serverless-функций на облачных инфраструктурах на основе импортозамещения". Разрабатываемый программный продукт представляет собой программный комплекс для автоматизированного запуска и замера производительности serverless-функций на облачной инфраструктуре импортозамещения, включающий набор тестовых сценариев, модуль сбора метрик и инструмент сравнения показателей до и после оптимизации. В качестве пользовательского интерфейса реализуется веб-портал с визуализацией результатов испытаний и формулировкой рекомендаций по эффективному развертыванию и конфигурации функций. Тема моей ВКР - "Оптимизация выполнения serverless-функций на облачных инфраструктурах на основе импортозамещения". Разрабатываемый программный продукт представляет собой программный комплекс для автоматизированного запуска и замера производительности serverless-функций на облачной инфраструктуре импортозамещения, включающий набор тестовых сценариев, модуль сбора метрик и инструмент сравнения показателей до и после оптимизации. В качестве пользовательского интерфейса реализуется веб-портал с визуализацией результатов испытаний и формулировкой рекомендаций по эффективному развертыванию и конфигурации функций.
# Диаграммы # Диаграммы
...@@ -22,26 +24,6 @@ ...@@ -22,26 +24,6 @@
## Диаграмма вариантов использования (Use Case) ## Диаграмма вариантов использования (Use Case)
Показывает, кто (акторы) взаимодействует с системой и какие функции (варианты использования) она поддерживает. Помогает зафиксировать требования на высоком уровне: какие сценарии выполнения задач имеются у пользователей и как они входят/выходят за границы системы. Показывает, кто (акторы) взаимодействует с системой и какие функции (варианты использования) она поддерживает. Помогает зафиксировать требования на высоком уровне: какие сценарии выполнения задач имеются у пользователей и как они входят/выходят за границы системы.
### Исходный код диаграммы
```plantuml
@startuml UseCase
title Диаграмма вариантов использования комплекса тестирования Serverless
left to right direction
actor ОблачныйИнженер as "Облачный инженер"
rectangle "Комплекс тестирования Serverless" {
(Настроить тестовые сценарии) as UC1
(Запустить тесты) as UC2
(Просмотреть результаты) as UC3
(Сгенерировать рекомендации) as UC4
}
ОблачныйИнженер --> UC1
ОблачныйИнженер --> UC2
ОблачныйИнженер --> UC3
ОблачныйИнженер --> UC4
@enduml
```
### Результат ### Результат
![Use Case Diagram](diagrams/UseCase.png) ![Use Case Diagram](diagrams/UseCase.png)
...@@ -49,54 +31,6 @@ rectangle "Комплекс тестирования Serverless" { ...@@ -49,54 +31,6 @@ rectangle "Комплекс тестирования Serverless" {
## Диаграмма классов (Class Diagram) ## Диаграмма классов (Class Diagram)
Отображает статическую структуру системы: её классы (или сущности), их атрибуты и методы, а также связи между ними (наследование, ассоциации, зависимости и т. д.). Помогает понять, из каких компонентов строится приложение и как они взаимодействуют на уровне типов. Отображает статическую структуру системы: её классы (или сущности), их атрибуты и методы, а также связи между ними (наследование, ассоциации, зависимости и т. д.). Помогает понять, из каких компонентов строится приложение и как они взаимодействуют на уровне типов.
### Исходный код диаграммы
```plantuml
@startuml ClassDiagram
title Диаграмма классов системы тестирования Serverless
class СценарийТеста {
+id: String
+имя: String
+конфигурация: Map<String, Object>
+выполнить(): Результат
}
class Оптимизатор {
+оптимизировать(сценарий: СценарийТеста): СценарийТеста
}
class СборщикМетрик {
+собратьМетрики(результат: Результат): Метрики
}
class ИнструментСравнения {
+сравнить(до: Метрики, после: Метрики): ОтчетСравнения
}
class ГенераторРекомендаций {
+сгенерироватьРекомендации(отчет: ОтчетСравнения): List<String>
}
class ВебПортал {
+показатьРезультаты(отчет: ОтчетСравнения)
+показатьРекомендации(рекомендации: List<String>)
}
class Результат {
+данные: Object
}
class Метрики {
+время: Integer
+ресурсы: Map<String, Integer>
}
class ОтчетСравнения {
+улучшения: Map<String, Double>
}
СценарийТеста --> Результат : "1"
Оптимизатор --> СценарийТеста : оптимизирует
СборщикМетрик --> Метрики : собирает
ИнструментСравнения --> ОтчетСравнения : создает отчет
ГенераторРекомендаций --> "List<String>" : рекомендации
ВебПортал --> ОтчетСравнения : отображает отчет
ВебПортал --> "List<String>" : отображает рекомендации
@enduml
```
### Результат ### Результат
![Class Diagram](diagrams/ClassDiagram.png) ![Class Diagram](diagrams/ClassDiagram.png)
...@@ -104,89 +38,29 @@ class ОтчетСравнения { ...@@ -104,89 +38,29 @@ class ОтчетСравнения {
## Диаграмма последовательности (Sequence Diagram) ## Диаграмма последовательности (Sequence Diagram)
Показывает динамику взаимодействия между объектами или компонентами в ходе выполнения конкретного сценария. По горизонтали — участники, по вертикали — временная шкала: кто и в каком порядке вызывает какие методы, и какие ответы получает. Показывает динамику взаимодействия между объектами или компонентами в ходе выполнения конкретного сценария. По горизонтали — участники, по вертикали — временная шкала: кто и в каком порядке вызывает какие методы, и какие ответы получает.
### Исходный код диаграммы
```plantuml
@startuml SequenceDiagram
title Диаграмма последовательности выполнения тестов Serverless
actor ОблачныйИнженер
participant ВебПортал
participant ДвижокТестирования as TestEngine
participant Оптимизатор
participant СборщикМетрик
participant ИнструментСравнения
participant ГенераторРекомендаций
ОблачныйИнженер -> ВебПортал: настроитьТест(конфигСценария)
ВебПортал -> ДвижокТестирования: запуститьТест(конфигСценария)
ДвижокТестирования -> Оптимизатор: оптимизировать(сценарий)
Оптимизатор --> ДвижокТестирования: оптимизированныйСценарий
ДвижокТестирования -> СборщикМетрик: собратьМетрики(оптимизированныйСценарий)
СборщикМетрик --> ДвижокТестирования: метрикиОптимизированного
ДвижокТестирования --> ВебПортал: результатыОптимизированного
ВебПортал -> ДвижокТестирования: запуститьТест(оригСценарий)
ДвижокТестирования -> СборщикМетрик: собратьМетрики(оригСценарий)
СборщикМетрик --> ДвижокТестирования: метрикиОригинала
ДвижокТестирования -> ИнструментСравнения: сравнить(метрикиОригинала, метрикиОптимизированного)
ИнструментСравнения --> ВебПортал: отчетСравнения
ВебПортал -> ГенераторРекомендаций: сгенерироватьРекомендации(отчетСравнения)
ГенераторРекомендаций --> ВебПортал: рекомендации
ВебПортал --> ОблачныйИнженер: отобразить(отчетСравнения, рекомендации)
@enduml
```
### Результат ### Результат
![Sequence Diagram](diagrams/SequenceDiagram.png) ![Sequence Diagram - Optimization](diagrams/Sequence_Optimization.png)
![Sequence Diagram - Testing](diagrams/Sequence_Testing.png)
![Sequence Diagram - Results](diagrams/Sequence_Results.png)
## Диаграмма состояний (State Diagram) ## Диаграмма состояний (State Diagram)
Описывает жизненный цикл единственного объекта или сущности: набор возможных состояний, в которых он может находиться, и переходы между ними (события и условия перехода). Полезна для моделирования поведения компонентов с явно выраженными стадиями. Описывает жизненный цикл единственного объекта или сущности: набор возможных состояний, в которых он может находиться, и переходы между ними (события и условия перехода). Полезна для моделирования поведения компонентов с явно выраженными стадиями.
### Исходный код диаграммы
```plantuml
@startuml StateDiagram
title Диаграмма состояний процесса тестирования Serverless
[*] --> Создано
Создано --> Оптимизировано : оптимизировать()
Оптимизировано --> Выполняется : выполнить()
Выполняется --> МетрикиСобраны : собратьМетрики()
МетрикиСобраны --> Сравнение : сравнить()
Сравнение --> Рекомендации : сгенерироватьРекомендации()
Рекомендации --> Просмотр : просмотреть()
Просмотр --> [*]
@enduml
```
### Результат ### Результат
![State Diagram](diagrams/StateDiagram.png) ![State Diagram - Editor](diagrams/State_Editor.png)
![State Diagram - Testing](diagrams/State_Testing.png)
![State Diagram - Results](diagrams/State_Results.png)
## Диаграмма деятельности (Activity Diagram) ## Диаграмма деятельности (Activity Diagram)
Моделирует процесс или рабочий поток: последовательность действий (активностей), ветвления, параллельных потоков и точек синхронизации. Помогает визуализировать бизнес-процессы или алгоритмы внутри системы. Моделирует процесс или рабочий поток: последовательность действий (активностей), ветвления, параллельных потоков и точек синхронизации. Помогает визуализировать бизнес-процессы или алгоритмы внутри системы.
### Исходный код диаграммы
```plantuml
@startuml ActivityDiagram
title Диаграмма активности процесса тестирования Serverless
start
:Настроить тестовый сценарий;
:Оптимизировать сценарий;
:Запустить оптимизированный тест;
:Собрать метрики оптимизированного теста;
:Запустить оригинальный тест;
:Собрать метрики оригинального теста;
:Сравнить метрики;
:Сгенерировать рекомендации;
:Отобразить результаты;
stop
@enduml
```
### Результат ### Результат
![Activity Diagram](diagrams/ActivityDiagram.png) ![Activity Diagram - Editor](diagrams/Activity_Editor.png)
![Activity Diagram - Testing](diagrams/Activity_Testing.png)
![Activity Diagram - Results](diagrams/Activity_Results.png)
# Код на Python # Код на Python
...@@ -197,62 +71,115 @@ stop ...@@ -197,62 +71,115 @@ stop
### Результат ### Результат
```python ```python
from typing import Any from datetime import datetime
from dataclasses import dataclass from typing import Any, Dict, List, Optional, Union
from enum import Enum
@dataclass
class Result: class Function:
data: Any def __init__(self, id: str, name: str, language: str, platform: str) -> None:
self.id: str = id
self.name: str = name
@dataclass self.language: str = language
class Metrics: self.platform: str = platform
time: int
resources: dict[str, int]
class Version:
def __init__(
@dataclass self, function: Function, version_number: str, origin: str, code: str, created_at: datetime
class ComparisonReport: ) -> None:
improvements: dict[str, float] self.function: Function = function
self.version_number: str = version_number
self.origin: str = origin
class TestScenario: self.code: str = code
def __init__(self, id: str, name: str, configuration: dict[str, Any]) -> None: self.created_at: datetime = created_at
self.id = id
self.name = name
self.configuration = configuration class OptimizationReport:
def __init__(
def execute(self) -> Result: self,
pass version: Version,
report_id: str,
recommendations: List[str],
class Optimizer: optimized_code: str,
def optimize(self, scenario: TestScenario) -> TestScenario: metrics_before: Dict[str, Any],
pass metrics_after: Dict[str, Any],
created_at: datetime,
) -> None:
class MetricsCollector: self.version: Version = version
def collect_metrics(self, result: Result) -> Metrics: self.report_id: str = report_id
pass self.recommendations: List[str] = recommendations
self.optimized_code: str = optimized_code
self.metrics_before: Dict[str, Any] = metrics_before
class ComparisonTool: self.metrics_after: Dict[str, Any] = metrics_after
def compare(self, before: Metrics, after: Metrics) -> ComparisonReport: self.created_at: datetime = created_at
pass
class TestConfiguration:
class RecommendationGenerator: def __init__(
def generate_recommendations(self, report: ComparisonReport) -> list[str]: self, version: Version, runtime: str, iterations: int, parallelism: int, timeout: int
pass ) -> None:
self.version: Version = version
self.runtime: str = runtime
class WebPortal: self.iterations: int = iterations
def show_results(self, report: ComparisonReport) -> None: self.parallelism: int = parallelism
pass self.timeout: int = timeout
def show_recommendations(self, recommendations: list[str]) -> None:
pass class TestRunStatus(Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class TestRun:
def __init__(
self,
test_configuration: TestConfiguration,
run_id: str,
started_at: datetime,
ended_at: Optional[datetime] = None,
status: TestRunStatus = TestRunStatus.PENDING,
) -> None:
self.test_configuration: TestConfiguration = test_configuration
self.run_id: str = run_id
self.started_at: datetime = started_at
self.ended_at: Optional[datetime] = ended_at
self.status: TestRunStatus = status
class Metric:
def __init__(
self, test_run: TestRun, type: str, percentiles: Dict[str, float], timestamp: datetime
) -> None:
self.test_run: TestRun = test_run
self.type: str = type
self.percentiles: Dict[str, float] = percentiles
self.timestamp: datetime = timestamp
class TestProfile:
def __init__(
self,
test_run: TestRun,
profile_id: str,
function_name: str,
version_number: str,
run_timestamp: datetime,
) -> None:
self.test_run: TestRun = test_run
self.profile_id: str = profile_id
self.function_name: str = function_name
self.version_number: str = version_number
self.run_timestamp: datetime = run_timestamp
class Draft:
def __init__(self, version: Version, content: str, last_updated: datetime) -> None:
self.version: Version = version
self.content: str = content
self.last_updated: datetime = last_updated
``` ```
# Прототип интерфейса # Прототип интерфейса
......
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from enum import Enum
class Function:
def __init__(self, id: str, name: str, language: str, platform: str) -> None:
self.id: str = id
self.name: str = name
self.language: str = language
self.platform: str = platform
class Version:
def __init__(
self, function: Function, version_number: str, origin: str, code: str, created_at: datetime
) -> None:
self.function: Function = function
self.version_number: str = version_number
self.origin: str = origin
self.code: str = code
self.created_at: datetime = created_at
class OptimizationReport:
def __init__(
self,
version: Version,
report_id: str,
recommendations: List[str],
optimized_code: str,
metrics_before: Dict[str, Any],
metrics_after: Dict[str, Any],
created_at: datetime,
) -> None:
self.version: Version = version
self.report_id: str = report_id
self.recommendations: List[str] = recommendations
self.optimized_code: str = optimized_code
self.metrics_before: Dict[str, Any] = metrics_before
self.metrics_after: Dict[str, Any] = metrics_after
self.created_at: datetime = created_at
class TestConfiguration:
def __init__(
self, version: Version, runtime: str, iterations: int, parallelism: int, timeout: int
) -> None:
self.version: Version = version
self.runtime: str = runtime
self.iterations: int = iterations
self.parallelism: int = parallelism
self.timeout: int = timeout
class TestRunStatus(Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class TestRun:
def __init__(
self,
test_configuration: TestConfiguration,
run_id: str,
started_at: datetime,
ended_at: Optional[datetime] = None,
status: TestRunStatus = TestRunStatus.PENDING,
) -> None:
self.test_configuration: TestConfiguration = test_configuration
self.run_id: str = run_id
self.started_at: datetime = started_at
self.ended_at: Optional[datetime] = ended_at
self.status: TestRunStatus = status
class Metric:
def __init__(
self, test_run: TestRun, type: str, percentiles: Dict[str, float], timestamp: datetime
) -> None:
self.test_run: TestRun = test_run
self.type: str = type
self.percentiles: Dict[str, float] = percentiles
self.timestamp: datetime = timestamp
class TestProfile:
def __init__(
self,
test_run: TestRun,
profile_id: str,
function_name: str,
version_number: str,
run_timestamp: datetime,
) -> None:
self.test_run: TestRun = test_run
self.profile_id: str = profile_id
self.function_name: str = function_name
self.version_number: str = version_number
self.run_timestamp: datetime = run_timestamp
class Draft:
def __init__(self, version: Version, content: str, last_updated: datetime) -> None:
self.version: Version = version
self.content: str = content
self.last_updated: datetime = last_updated
\ No newline at end of file
from typing import Any
from dataclasses import dataclass
@dataclass
class Result:
data: Any
@dataclass
class Metrics:
time: int
resources: dict[str, int]
@dataclass
class ComparisonReport:
improvements: dict[str, float]
class TestScenario:
def __init__(self, id: str, name: str, configuration: dict[str, Any]) -> None:
self.id = id
self.name = name
self.configuration = configuration
def execute(self) -> Result:
pass
class Optimizer:
def optimize(self, scenario: TestScenario) -> TestScenario:
pass
class MetricsCollector:
def collect_metrics(self, result: Result) -> Metrics:
pass
class ComparisonTool:
def compare(self, before: Metrics, after: Metrics) -> ComparisonReport:
pass
class RecommendationGenerator:
def generate_recommendations(self, report: ComparisonReport) -> list[str]:
pass
class WebPortal:
def show_results(self, report: ComparisonReport) -> None:
pass
def show_recommendations(self, recommendations: list[str]) -> None:
pass
\ No newline at end of file
diagrams/ClassDiagram.png

71.3 KB | W: | H:

diagrams/ClassDiagram.png

45 KB | W: | H:

diagrams/ClassDiagram.png
diagrams/ClassDiagram.png
diagrams/ClassDiagram.png
diagrams/ClassDiagram.png
  • 2-up
  • Swipe
  • Onion skin
diagrams/UseCase.png

29.4 KB | W: | H:

diagrams/UseCase.png

71.9 KB | W: | H:

diagrams/UseCase.png
diagrams/UseCase.png
diagrams/UseCase.png
diagrams/UseCase.png
  • 2-up
  • Swipe
  • Onion skin
@startuml ActivityDiagram @startuml Activity_Editor
title Диаграмма активности процесса тестирования Serverless title Диаграмма деятельности: Редактор кода
start partition Пользователь {
:Настроить тестовый сценарий; start
:Оптимизировать сценарий; :Открыть страницу "Редактор";
:Запустить оптимизированный тест; if (Режим = Create?) then (да)
:Собрать метрики оптимизированного теста; :Заполнить "Название";
:Запустить оригинальный тест; :Выбрать "Язык" и "Платформу";
:Собрать метрики оригинального теста; :Ввести код функции;
:Сравнить метрики; if (Код ≠ пустой) then (да)
:Сгенерировать рекомендации; :Нажать "Сохранить";
:Отобразить результаты; else
:Показать ошибку "Код не может быть пустым";
stop
endif
else (нет)
:Просмотреть существующий код;
endif
:Нажать кнопку "Редактировать" или "Оптимизировать";
}
partition Система {
if (Нажали "Оптимизировать") then (да)
:Отправить код в LLM;
:Получить оптимизированный код и рекомендации;
:Перейти в режим ViewOptimized;
elseif (Нажали "Редактировать")
:Перейти в режим Edit;
endif
if (В режиме Edit и есть изменения?) then (да)
:Сохранить новую версию;
:Перейти в режим View;
endif
}
stop stop
@enduml @enduml
\ No newline at end of file
@startuml Activity_Testing
title Диаграмма деятельности: Тестирование
partition Пользователь {
start
:Открыть страницу "Тестирование";
:Выбрать функцию и версию;
:Задать параметры теста (рантайм, итерации, параллельность, таймаут);
if (Все поля заполнены?) then (да)
:Нажать "Запустить тестирование";
else
:Показать тултип "Заполните все поля";
stop
endif
}
partition Система {
:Отправить запрос на запуск тестов;
:Отобразить прогресс-бар и секундомер;
while (Тестирование в процессе)
:Собирать метрики;
endwhile
}
partition Пользователь {
if (Нажали "Отмена" во время теста?) then (да)
:Подтвердить отмену;
stop
endif
}
partition Система {
:Тестирование завершено;
}
partition Пользователь {
:Показать экран "Результаты тестирования";
if (Нажали "Просмотр результатов") then (да)
:Перейти на вкладку "Результаты";
elseif (Нажали "Новый тест")
:Вернуться к конфигурации;
endif
}
stop
@enduml
@startuml Activity_Results
title Диаграмма деятельности: Результаты
partition Пользователь {
start
:Открыть страницу "Результаты";
:Показать два селектора профилей;
:Ожидать выбор в Селекторе 1;
if (Профиль 1 выбран?) then (да)
:Разблокировать Селектор 2;
else
stop
endif
:Ожидать выбор в Селекторе 2?;
}
partition Система {
if (Профиль 1 выбран) then
:Загрузить и отобразить данные профиля 1;
:Показать графики и "Детали теста 1";
endif
if (Профиль 2 выбран) then
:Загрузить и отобразить данные профиля 2;
:Показать графики сравнения;
:Показать "Детали теста 2" и "Сводку изменений";
endif
}
partition Пользователь {
:Сбросить селекторы или закрыть страницу;
}
stop
@enduml
\ No newline at end of file
@startuml ClassDiagram @startuml ClassDiagram
title Диаграмма классов системы тестирования Serverless title Диаграмма классов: Система тестирования Serverless
class СценарийТеста { class Function {
+id: String +id
+имя: String +name
+конфигурация: Map<String, Object> +language
+выполнить(): Результат +platform
} }
class Оптимизатор {
+оптимизировать(сценарий: СценарийТеста): СценарийТеста class Version {
} +versionNumber
class СборщикМетрик { +origin
+собратьМетрики(результат: Результат): Метрики +code
+createdAt
} }
class ИнструментСравнения {
+сравнить(до: Метрики, после: Метрики): ОтчетСравнения class OptimizationReport {
+reportId
+recommendations
+optimizedCode
+metricsBefore
+metricsAfter
+createdAt
} }
class ГенераторРекомендаций {
+сгенерироватьРекомендации(отчет: ОтчетСравнения): List<String> class TestConfiguration {
+runtime
+iterations
+parallelism
+timeout
} }
class ВебПортал {
+показатьРезультаты(отчет: ОтчетСравнения) class TestRun {
+показатьРекомендации(рекомендации: List<String>) +runId
+startedAt
+endedAt
+status
} }
class Результат {
+данные: Object class Metric {
+type
+percentiles
+timestamp
} }
class Метрики {
+время: Integer class TestProfile {
+ресурсы: Map<String, Integer> +profileId
+functionName
+versionNumber
+runTimestamp
} }
class ОтчетСравнения {
+улучшения: Map<String, Double> class Draft {
+content
+lastUpdated
} }
СценарийТеста --> Результат : "1" Function "1" -- "0..*" Version
Оптимизатор --> СценарийТеста : оптимизирует Version "1" -- "0..1" OptimizationReport
СборщикМетрик --> Метрики : собирает Version "1" -- "0..*" TestConfiguration
ИнструментСравнения --> ОтчетСравнения : создает отчет TestConfiguration "1" -- "1..*" TestRun
ГенераторРекомендаций --> "List<String>" : рекомендации TestRun "1" -- "0..*" Metric
ВебПортал --> ОтчетСравнения : отображает отчет TestRun "1" -- "0..1" TestProfile
ВебПортал --> "List<String>" : отображает рекомендации Version "1" -- "0..1" Draft
@enduml @enduml
\ No newline at end of file \ No newline at end of file
@startuml SequenceDiagram @startuml Sequence_Optimization
title Диаграмма последовательности выполнения тестов Serverless title Диаграмма последовательности: Оптимизация кода
actor ОблачныйИнженер actor "Разработчик" as Dev
participant ВебПортал participant "Веб-интерфейс" as UI
participant ДвижокТестирования as TestEngine participant "Backend-сервис" as Backend
participant Оптимизатор participant "LLM-модуль" as LLM
participant СборщикМетрик
participant ИнструментСравнения
participant ГенераторРекомендаций
ОблачныйИнженер -> ВебПортал: настроитьТест(конфигСценария) Dev -> UI : Нажатие кнопки "Оптимизировать"
ВебПортал -> ДвижокТестирования: запуститьТест(конфигСценария) UI -> Backend : Отправить код функции
ДвижокТестирования -> Оптимизатор: оптимизировать(сценарий) Backend -> LLM : Запрос на оптимизацию
Оптимизатор --> ДвижокТестирования: оптимизированныйСценарий LLM --> Backend : Переработанный код + рекомендации
ДвижокТестирования -> СборщикМетрик: собратьМетрики(оптимизированныйСценарий) Backend --> UI : Отобразить новую версию и блок "Оптимизации"
СборщикМетрик --> ДвижокТестирования: метрикиОптимизированного @enduml
ДвижокТестирования --> ВебПортал: результатыОптимизированного
ВебПортал -> ДвижокТестирования: запуститьТест(оригСценарий) @startuml Sequence_Testing
ДвижокТестирования -> СборщикМетрик: собратьМетрики(оригСценарий) title Диаграмма последовательности: Запуск тестирования
СборщикМетрик --> ДвижокТестирования: метрикиОригинала actor "Инженер по качеству" as QA
ДвижокТестирования -> ИнструментСравнения: сравнить(метрикиОригинала, метрикиОптимизированного) participant "Веб-интерфейс" as UI
ИнструментСравнения --> ВебПортал: отчетСравнения participant "Backend-сервис" as Backend
ВебПортал -> ГенераторРекомендаций: сгенерироватьРекомендации(отчетСравнения) participant "Облачная платформа" as Cloud
ГенераторРекомендаций --> ВебПортал: рекомендации participant "Модуль сбора метрик" as Metrics
ВебПортал --> ОблачныйИнженер: отобразить(отчетСравнения, рекомендации)
@enduml QA -> UI : Нажать "Запустить тестирование"
\ No newline at end of file UI -> Backend : Передать параметры теста
Backend -> Cloud : Запустить функцию (итерации, параллельность)
Cloud --> Metrics : Передача метрик во время выполнения
Metrics --> Backend : Сбор и агрегация результатов
Backend --> UI : Передать данные для отображения
@enduml
@startuml Sequence_Results
title Диаграмма последовательности: Сравнение результатов
actor "Разработчик" as Dev
participant "Веб-интерфейс" as UI
participant "Backend-сервис" as Backend
Dev -> UI : Выбор двух профилей тестирования
UI -> Backend : Запрос данных двух профилей
Backend --> UI : Данные профилей + дельты
UI --> Dev : Отобразить графики и "Сводку изменений"
@enduml
\ No newline at end of file
@startuml StateDiagram @startuml State_Editor
title Диаграмма состояний процесса тестирования Serverless title Диаграмма состояний: Редактор кода
[*] --> Создано skinparam state {
Создано --> Оптимизировано : оптимизировать() BackgroundColor #FDF6E3
Оптимизировано --> Выполняется : выполнить() BorderColor #586E75
Выполняется --> МетрикиСобраны : собратьМетрики() }
МетрикиСобраны --> Сравнение : сравнить()
Сравнение --> Рекомендации : сгенерироватьРекомендации() [*] --> Create : Открыть страницу в режиме Create
Рекомендации --> Просмотр : просмотреть()
Просмотр --> [*] state Create {
@enduml [*] --> Создание
\ No newline at end of file Создание --> Сохранено : Нажать «Сохранить» (код ≠ пустой)
}
Create --> View : Сохранено → View
state View {
[*] --> Просмотр
Просмотр --> Edit : Нажать «Редактировать»
Просмотр --> ViewOptimized : Нажать «Оптимизировать»
}
state ViewOptimized {
[*] --> Оптимизировано
Оптимизировано --> Edit : Нажать «Редактировать»
}
state Edit {
[*] --> Редактирование
Редактирование --> View : Нажать «Сохранить» (есть изменения)
}
@enduml
@startuml State_Testing
title Диаграмма состояний: Тестирование
skinparam state {
BackgroundColor #EEF6F7
BorderColor #2AA198
}
[*] --> Configuration : Открыть страницу «Тестирование»
state Configuration {
[*] --> Конфигурирование
Конфигурирование --> Testing : Нажать «Запустить тестирование» (все поля заполнены)
}
state Testing {
[*] --> Запущено
Запущено --> Configuration : Отмена пользователя
Запущено --> Results : Тестирование завершено
}
state Results {
[*] --> Отображение
Отображение --> Configuration : Нажать «Новый тест»
Отображение --> ResultsPage : Нажать «Просмотр результатов»
}
@enduml
@startuml State_Results
title Диаграмма состояний: Результаты
skinparam state {
BackgroundColor #ECEBE4
BorderColor #657B83
}
[*] --> Selection : Открыть страницу «Результаты»
state Selection {
[*] --> ОжиданиеВыбора
ОжиданиеВыбора --> View : Выбрать профиль в Селекторе 1
}
state View {
[*] --> Просмотр
Просмотр --> Selection : Сбросить Селектор 1
Просмотр --> Compare : Выбрать профиль в Селекторе 2
}
state Compare {
[*] --> Сравнение
Сравнение --> View : Сбросить Селектор 2
Сравнение --> Selection : Сбросить Селектор 1
}
@enduml
\ No newline at end of file
@startuml UseCase @startuml UseCase
title Диаграмма вариантов использования комплекса тестирования Serverless title Диаграмма вариантов использования: Тестирование Serverless
left to right direction left to right direction
actor ОблачныйИнженер as "Облачный инженер" skinparam packageStyle rectangle
rectangle "Комплекс тестирования Serverless" {
(Настроить тестовые сценарии) as UC1 actor "Разработчик" as Developer
(Запустить тесты) as UC2 actor "Инженер по качеству" as QA
(Просмотреть результаты) as UC3
(Сгенерировать рекомендации) as UC4 rectangle "Управление функциями" {
} usecase "Создать новую функцию" as UC1
ОблачныйИнженер --> UC1 usecase "Просмотреть список функций" as UC2
ОблачныйИнженер --> UC2 usecase "Редактировать функцию" as UC3
ОблачныйИнженер --> UC3 }
ОблачныйИнженер --> UC4
@enduml rectangle "Оптимизация функции" {
\ No newline at end of file usecase "Запустить авто-оптимизацию" as UC4
usecase "Применить рекомендации LLM" as UC5
}
rectangle "Тестирование" {
usecase "Настроить параметры тестирования" as UC6
usecase "Запустить нагрузочное тестирование" as UC7
}
rectangle "Анализ результатов" {
usecase "Просмотреть результаты теста" as UC8
usecase "Сравнить профили тестирования" as UC9
}
Developer --> UC1
Developer --> UC2
Developer --> UC3
Developer --> UC4
Developer --> UC5
QA --> UC6
QA --> UC7
Developer --> UC8
Developer --> UC9
@enduml
# Node.js
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn
.yarn-integrity
# dotenv environment variables file
.env
# MacOS
.DS_Store
# Editor directories and files
.idea/
.vscode/
*.sublime-workspace
# Build directories
build/
dist/
# Docker
*.container
*.tar
\ No newline at end of file
FROM node:20-alpine
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# No need to copy source files as they will be mounted as a volume
# Expose port for development server
EXPOSE 3000
# Start development server with hot-reloading
CMD ["npm", "start"]
\ No newline at end of file
...@@ -2,10 +2,18 @@ version: '3.8' ...@@ -2,10 +2,18 @@ version: '3.8'
services: services:
frontend: frontend:
build: . build:
context: .
dockerfile: Dockerfile.dev
ports: ports:
- "8080:80" - "3000:3000"
restart: unless-stopped restart: unless-stopped
volumes:
- ./:/app
- /app/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
- NODE_ENV=development
networks: networks:
- serverless-net - serverless-net
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,7 +6,9 @@ ...@@ -6,7 +6,9 @@
"@emotion/react": "^11.11.0", "@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16", "@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^5.13.2", "@mui/material": "^5.13.2",
"@uiw/react-textarea-code-editor": "^3.1.1",
"chart.js": "^4.3.0", "chart.js": "^4.3.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
...@@ -38,4 +40,4 @@ ...@@ -38,4 +40,4 @@
"last 1 safari version" "last 1 safari version"
] ]
} }
} }
\ No newline at end of file
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<title>Платформа тестирования Serverless</title> <title>Платформа тестирования Serverless</title>
</head> </head>
<body> <body>
......
\ No newline at end of file
...@@ -4,10 +4,10 @@ import { ThemeProvider, createTheme } from '@mui/material/styles'; ...@@ -4,10 +4,10 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import Layout from './components/Layout'; import Layout from './components/Layout';
import HomePage from './pages/HomePage'; import FunctionsPage from './pages/FunctionsPage';
import OptimizationPage from './pages/OptimizationPage';
import TestingPage from './pages/TestingPage'; import TestingPage from './pages/TestingPage';
import ResultsPage from './pages/ResultsPage'; import ResultsPage from './pages/ResultsPage';
import ConfigurationPage from './pages/ConfigurationPage';
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
...@@ -32,10 +32,13 @@ function App() { ...@@ -32,10 +32,13 @@ function App() {
<CssBaseline /> <CssBaseline />
<Layout> <Layout>
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<FunctionsPage />} />
<Route path="/optimization" element={<OptimizationPage />} />
<Route path="/optimization/:functionId" element={<OptimizationPage />} />
<Route path="/optimization/:functionId/:version" element={<OptimizationPage />} />
<Route path="/testing" element={<TestingPage />} /> <Route path="/testing" element={<TestingPage />} />
<Route path="/testing/:functionId/:version" element={<TestingPage />} />
<Route path="/results" element={<ResultsPage />} /> <Route path="/results" element={<ResultsPage />} />
<Route path="/configuration" element={<ConfigurationPage />} />
</Routes> </Routes>
</Layout> </Layout>
</ThemeProvider> </ThemeProvider>
......
...@@ -15,18 +15,18 @@ import { ...@@ -15,18 +15,18 @@ import {
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import HomeIcon from '@mui/icons-material/Home'; import FunctionsIcon from '@mui/icons-material/Code';
import BuildIcon from '@mui/icons-material/Build';
import SpeedIcon from '@mui/icons-material/Speed'; import SpeedIcon from '@mui/icons-material/Speed';
import BarChartIcon from '@mui/icons-material/BarChart'; import BarChartIcon from '@mui/icons-material/BarChart';
import SettingsIcon from '@mui/icons-material/Settings';
const drawerWidth = 240; const drawerWidth = 240;
const menuItems = [ const menuItems = [
{ text: 'Главная', icon: <HomeIcon />, path: '/' }, { text: 'Функции', icon: <FunctionsIcon />, path: '/' },
{ text: 'Оптимизация', icon: <BuildIcon />, path: '/optimization' },
{ text: 'Тестирование', icon: <SpeedIcon />, path: '/testing' }, { text: 'Тестирование', icon: <SpeedIcon />, path: '/testing' },
{ text: 'Результаты', icon: <BarChartIcon />, path: '/results' }, { text: 'Результаты', icon: <BarChartIcon />, path: '/results' },
{ text: 'Конфигурация', icon: <SettingsIcon />, path: '/configuration' },
]; ];
function Layout({ children }) { function Layout({ children }) {
...@@ -40,11 +40,18 @@ function Layout({ children }) { ...@@ -40,11 +40,18 @@ function Layout({ children }) {
setMobileOpen(!mobileOpen); setMobileOpen(!mobileOpen);
}; };
const isPathActive = (path) => {
if (path === '/') {
return location.pathname === '/';
}
return location.pathname.startsWith(path);
};
const drawer = ( const drawer = (
<div> <div>
<Toolbar sx={{ justifyContent: 'center' }}> <Toolbar sx={{ justifyContent: 'center' }}>
<Typography variant="h6" component="div" fontWeight="bold"> <Typography variant="h6" component="div" fontWeight="bold">
ServerlessTest Serverless Optimizer
</Typography> </Typography>
</Toolbar> </Toolbar>
<List> <List>
...@@ -53,7 +60,7 @@ function Layout({ children }) { ...@@ -53,7 +60,7 @@ function Layout({ children }) {
button button
key={item.text} key={item.text}
onClick={() => navigate(item.path)} onClick={() => navigate(item.path)}
selected={location.pathname === item.path} selected={isPathActive(item.path)}
> >
<ListItemIcon>{item.icon}</ListItemIcon> <ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} /> <ListItemText primary={item.text} />
...@@ -83,7 +90,7 @@ function Layout({ children }) { ...@@ -83,7 +90,7 @@ function Layout({ children }) {
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Тестирование Serverless-функций Оптимизация выполнения serverless-функций
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
......
...@@ -24,4 +24,35 @@ code { ...@@ -24,4 +24,35 @@ code {
.chart-container { .chart-container {
height: 300px; height: 300px;
position: relative; position: relative;
}
/* Code Editor Styles */
.w-tc-editor {
position: relative;
}
.line-numbers > div {
height: 21px; /* Match the line-height of the editor */
}
/* Ensure editor lines match height with line numbers */
.w-tc-editor pre {
line-height: 1.5;
}
/* Ensure consistent scrolling */
.w-tc-editor-text {
overflow-y: auto !important;
}
/* Enforce JetBrains Mono for the editor */
.w-tc-editor-text,
.w-tc-editor-preview {
font-family: 'JetBrains Mono', monospace !important;
}
/* Dark mode support */
.w-tc-editor[data-color-mode="dark"] .w-tc-editor-text > div::before {
color: #666;
border-right-color: #444;
} }
\ No newline at end of file
...@@ -14,4 +14,7 @@ root.render( ...@@ -14,4 +14,7 @@ root.render(
</React.StrictMode> </React.StrictMode>
); );
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(); reportWebVitals();
\ No newline at end of file
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
Button,
Container,
Divider,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Modal,
Paper,
Tooltip,
Typography,
Collapse,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CodeIcon from '@mui/icons-material/Code';
import JavascriptIcon from '@mui/icons-material/Javascript';
import CloudIcon from '@mui/icons-material/Cloud';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import VisibilityIcon from '@mui/icons-material/Visibility';
import SpeedIcon from '@mui/icons-material/Speed';
import Chip from '@mui/material/Chip';
// Моковые данные для отображения
const mockFunctions = [
{
id: 'func1',
name: 'API Gateway Handler',
language: 'python',
platform: 'yandex',
versions: [
{ version: 1, type: 'manual', createdAt: '2024-03-10' },
{ version: 2, type: 'auto', createdAt: '2024-03-11' },
{ version: 3, type: 'manual', createdAt: '2024-03-12' }
]
},
{
id: 'func2',
name: 'Database Worker',
language: 'nodejs',
platform: 'yandex',
versions: [
{ version: 1, type: 'manual', createdAt: '2024-03-14' },
{ version: 2, type: 'auto', createdAt: '2024-03-15' }
]
},
{
id: 'func3',
name: 'Image Processor',
language: 'python',
platform: 'yandex',
versions: [
{ version: 1, type: 'manual', createdAt: '2024-03-20' }
]
}
];
const modalStyle = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
borderRadius: 1,
boxShadow: 24,
p: 4,
};
function FunctionsPage() {
const navigate = useNavigate();
const [expandedFunctions, setExpandedFunctions] = useState({});
const [selectedVersion, setSelectedVersion] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
const toggleFunction = (id) => {
setExpandedFunctions(prev => ({
...prev,
[id]: !prev[id]
}));
};
const openVersionModal = (func, version) => {
setSelectedVersion({ functionId: func.id, functionName: func.name, ...version });
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
};
const handleViewClick = () => {
navigate(`/optimization/${selectedVersion.functionId}/${selectedVersion.version}`);
closeModal();
};
const handleTestClick = () => {
navigate(`/testing/${selectedVersion.functionId}/${selectedVersion.version}`);
closeModal();
};
const getLanguageIcon = (language) => {
switch (language) {
case 'python':
return <CodeIcon fontSize="small" color="primary" />;
case 'nodejs':
return <JavascriptIcon fontSize="small" color="warning" />;
default:
return <CodeIcon fontSize="small" />;
}
};
const getPlatformIcon = (platform) => {
return <CloudIcon fontSize="small" color="info" />;
};
const getVersionTypeChip = (type) => {
if (type === 'auto') {
return <Chip label="Auto" size="small" color="secondary" variant="outlined" sx={{ ml: 1 }} />;
}
return <Chip label="Manual" size="small" color="primary" variant="outlined" sx={{ ml: 1 }} />;
};
return (
<Container maxWidth="lg">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Сохраненные функции
</Typography>
<Paper elevation={2} sx={{ p: 0, mb: 4 }}>
<List sx={{ width: '100%' }}>
{mockFunctions.map((func) => (
<React.Fragment key={func.id}>
<ListItem
secondaryAction={
<IconButton edge="end" onClick={() => toggleFunction(func.id)}>
{expandedFunctions[func.id] ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
}
disablePadding
>
<ListItemButton onClick={() => toggleFunction(func.id)}>
<ListItemIcon>
{getLanguageIcon(func.language)}
</ListItemIcon>
<ListItemText
primary={func.name}
primaryTypographyProps={{ fontWeight: 'medium' }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', mr: 2 }}>
<Tooltip title={func.language === 'python' ? 'Python' : 'Node.js'}>
{getLanguageIcon(func.language)}
</Tooltip>
<Box sx={{ mx: 0.5 }} />
<Tooltip title="Yandex Cloud Functions">
{getPlatformIcon(func.platform)}
</Tooltip>
</Box>
</ListItemButton>
</ListItem>
<Collapse in={expandedFunctions[func.id]} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{func.versions.map((version) => (
<ListItem
key={`${func.id}-v${version.version}`}
sx={{ pl: 4 }}
secondaryAction={
<IconButton edge="end" onClick={() => openVersionModal(func, version)}>
<VisibilityIcon fontSize="small" />
</IconButton>
}
>
<ListItemButton onClick={() => openVersionModal(func, version)}>
<ListItemText
primary={`Версия ${version.version}`}
secondary={version.createdAt}
/>
{getVersionTypeChip(version.type)}
</ListItemButton>
</ListItem>
))}
</List>
</Collapse>
<Divider component="li" />
</React.Fragment>
))}
</List>
</Paper>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={() => navigate('/optimization')}
sx={{ mb: 4 }}
>
Новая функция
</Button>
<Modal
open={modalOpen}
onClose={closeModal}
aria-labelledby="version-modal-title"
>
<Box sx={modalStyle}>
<Typography id="version-modal-title" variant="h6" component="h2" gutterBottom>
{selectedVersion && `${selectedVersion.functionName} - Версия ${selectedVersion.version}`}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
{selectedVersion && `Создана: ${selectedVersion.createdAt}`}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 3 }}>
<Button
variant="outlined"
startIcon={<VisibilityIcon />}
onClick={handleViewClick}
>
Просмотр
</Button>
<Button
variant="contained"
startIcon={<SpeedIcon />}
onClick={handleTestClick}
>
Тестирование
</Button>
</Box>
</Box>
</Modal>
</Box>
</Container>
);
}
export default FunctionsPage;
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Button,
Container,
FormControl,
InputLabel,
MenuItem,
Paper,
Select,
TextField,
Typography,
Alert,
} from '@mui/material';
import CodeEditor from '@uiw/react-textarea-code-editor';
// Моковые данные для функций
const mockFunctions = {
'func1': {
name: 'API Gateway Handler',
language: 'python',
platform: 'yandex',
versions: {
1: {
code: `import json
def handler(event, context):
"""
Обработчик запросов API Gateway
"""
request_body = json.loads(event['body'])
user_id = request_body.get('user_id')
action = request_body.get('action')
# Проверка параметров
if not user_id or not action:
return {
'statusCode': 400,
'body': json.dumps({
'error': 'Missing required parameters'
})
}
# Обработка запроса
result = process_request(user_id, action)
# Возврат результата
return {
'statusCode': 200,
'body': json.dumps(result)
}
def process_request(user_id, action):
# Здесь могла бы быть бизнес-логика
return {
'user_id': user_id,
'action': action,
'status': 'processed'
}`,
type: 'manual'
},
2: {
code: `import json
def handler(event, context):
"""
Оптимизированный обработчик запросов API Gateway
"""
request_body = json.loads(event['body'])
user_id = request_body.get('user_id')
action = request_body.get('action')
# Быстрая проверка параметров
if not user_id or not action:
return {
'statusCode': 400,
'body': '{"error":"Missing required parameters"}'
}
# Обработка запроса
result = process_request(user_id, action)
# Возврат результата с предварительным форматированием JSON
return {
'statusCode': 200,
'body': json.dumps({
'user_id': user_id,
'action': action,
'status': 'processed'
})
}
def process_request(user_id, action):
# Оптимизированная бизнес-логика
return {
'user_id': user_id,
'action': action,
'status': 'processed'
}`,
type: 'auto',
optimizationDetails: [
'Уменьшено количество вызовов json.dumps для повышения производительности',
'Предварительное форматирование JSON в случае ошибки для экономии ресурсов',
'Оптимизирована структура возвращаемых данных для снижения задержек'
]
},
3: {
code: `import json
import time
def handler(event, context):
"""
Улучшенный обработчик запросов API Gateway с метриками
"""
start_time = time.time()
request_body = json.loads(event['body'])
user_id = request_body.get('user_id')
action = request_body.get('action')
# Быстрая проверка параметров
if not user_id or not action:
return {
'statusCode': 400,
'body': '{"error":"Missing required parameters"}'
}
# Обработка запроса
result = process_request(user_id, action)
# Добавление метрик производительности
execution_time = time.time() - start_time
# Возврат результата с предварительным форматированием JSON
return {
'statusCode': 200,
'body': json.dumps({
'user_id': user_id,
'action': action,
'status': 'processed',
'execution_time_ms': round(execution_time * 1000, 2)
})
}
def process_request(user_id, action):
# Оптимизированная бизнес-логика
return {
'user_id': user_id,
'action': action,
'status': 'processed'
}`,
type: 'manual'
}
}
},
'func2': {
name: 'Database Worker',
language: 'nodejs',
platform: 'yandex',
versions: {
1: {
code: `const { Client } = require('pg');
exports.handler = async function(event, context) {
// Создаем подключение к БД
const client = new Client({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
});
// Подключаемся к БД
await client.connect();
try {
// Получаем параметры из запроса
const requestData = JSON.parse(event.body);
const { query, params } = requestData;
// Выполняем запрос
const result = await client.query(query, params);
// Возвращаем результат
return {
statusCode: 200,
body: JSON.stringify(result.rows)
};
} catch (err) {
// Обработка ошибок
return {
statusCode: 500,
body: JSON.stringify({ error: err.message })
};
} finally {
// Закрываем соединение
await client.end();
}
};`,
type: 'manual'
},
2: {
code: `const { Pool } = require('pg');
// Создаем пул соединений для повторного использования
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
max: 20, // максимальное количество соединений
idleTimeoutMillis: 30000, // время ожидания перед закрытием неиспользуемых соединений
});
exports.handler = async function(event, context) {
// Получаем клиента из пула
const client = await pool.connect();
try {
// Получаем параметры из запроса
const requestData = JSON.parse(event.body);
const { query, params } = requestData;
// Выполняем запрос
const result = await client.query(query, params);
// Возвращаем результат
return {
statusCode: 200,
body: JSON.stringify(result.rows)
};
} catch (err) {
// Обработка ошибок
return {
statusCode: 500,
body: JSON.stringify({ error: err.message })
};
} finally {
// Возвращаем клиента в пул
client.release();
}
};`,
type: 'auto',
optimizationDetails: [
'Заменен Client на Pool для повторного использования соединений БД',
'Добавлены настройки пула для оптимального управления ресурсами',
'Использован метод client.release() вместо client.end() для возврата соединения в пул'
]
}
}
},
'func3': {
name: 'Image Processor',
language: 'python',
platform: 'yandex',
versions: {
1: {
code: `import io
import json
import base64
from PIL import Image
def handler(event, context):
"""
Обработчик изображений из запроса
"""
try:
# Декодируем изображение из base64
image_data = json.loads(event['body']).get('image')
if not image_data:
return {
'statusCode': 400,
'body': json.dumps({
'error': 'No image data provided'
})
}
# Декодируем base64 в байты
image_bytes = base64.b64decode(image_data)
# Открываем изображение с помощью PIL
image = Image.open(io.BytesIO(image_bytes))
# Обрабатываем изображение (пример: изменение размера)
processed_image = image.resize((300, 300))
# Сохраняем обработанное изображение в буфер
buffer = io.BytesIO()
processed_image.save(buffer, format='JPEG')
buffer.seek(0)
# Кодируем обратно в base64
processed_image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
return {
'statusCode': 200,
'body': json.dumps({
'processed_image': processed_image_base64
})
}
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps({
'error': str(e)
})
}`,
type: 'manual'
}
}
}
};
// Новый шаблон для создания функции
const emptyTemplates = {
'python': `# Введите свой код здесь
def handler(event, context):
"""
Функция-обработчик для Yandex Cloud Functions
"""
return {
'statusCode': 200,
'body': '{"message": "Hello from serverless function!"}'
}`,
'nodejs': `// Введите свой код здесь
exports.handler = async function(event, context) {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from serverless function!" })
};
};`
};
// Enum для состояний страницы
const PageState = {
CREATE: 'create',
VIEW: 'view',
VIEW_OPTIMIZED: 'view_optimized',
EDIT: 'edit'
};
function OptimizationPage() {
const { functionId, version } = useParams();
const navigate = useNavigate();
// Состояния для различных полей
const [pageState, setPageState] = useState(PageState.CREATE);
const [functionName, setFunctionName] = useState('');
const [language, setLanguage] = useState('python');
const [platform, setPlatform] = useState('yandex');
const [code, setCode] = useState(emptyTemplates.python);
const [currentVersion, setCurrentVersion] = useState(1);
const [optimizationDetails, setOptimizationDetails] = useState([]);
const [alertVisible, setAlertVisible] = useState(false);
// Ref для синхронизации прокрутки
const codeEditorRef = React.useRef(null);
const lineNumbersRef = React.useRef(null);
// Функция для синхронизации прокрутки
const handleScroll = React.useCallback((e) => {
if (lineNumbersRef.current) {
lineNumbersRef.current.scrollTop = e.target.scrollTop;
}
}, []);
// Обработка изменения состояния страницы на основе URL параметров
useEffect(() => {
if (functionId && version) {
// Получаем данные функции из моковых данных
const funcData = mockFunctions[functionId];
if (funcData && funcData.versions[version]) {
setFunctionName(funcData.name);
setLanguage(funcData.language);
setPlatform(funcData.platform);
setCode(funcData.versions[version].code);
setCurrentVersion(parseInt(version));
// Устанавливаем состояние на основе типа версии
if (funcData.versions[version].type === 'auto') {
setPageState(PageState.VIEW_OPTIMIZED);
setOptimizationDetails(funcData.versions[version].optimizationDetails || []);
} else {
setPageState(PageState.VIEW);
}
}
} else if (functionId && !version) {
// Если есть ID функции, но нет версии - открываем последнюю версию
const funcData = mockFunctions[functionId];
if (funcData) {
const latestVersion = Math.max(...Object.keys(funcData.versions).map(v => parseInt(v)));
navigate(`/optimization/${functionId}/${latestVersion}`);
}
} else {
// Если нет параметров - режим создания новой функции
setPageState(PageState.CREATE);
setFunctionName('');
setLanguage('python');
setPlatform('yandex');
setCode(emptyTemplates.python);
setCurrentVersion(1);
setOptimizationDetails([]);
}
}, [functionId, version, navigate]);
// Обновление шаблона кода при смене языка в режиме создания
useEffect(() => {
if (pageState === PageState.CREATE) {
setCode(emptyTemplates[language]);
}
}, [language, pageState]);
// Обработчики событий
const handleSaveFunction = () => {
// Здесь должен быть запрос к API для сохранения функции
// В данном прототипе просто показываем уведомление и меняем состояние
setAlertVisible(true);
setTimeout(() => setAlertVisible(false), 3000);
if (pageState === PageState.CREATE) {
// После создания переходим к просмотру
setPageState(PageState.VIEW);
} else if (pageState === PageState.EDIT) {
// После редактирования переходим к просмотру с новой версией
setCurrentVersion(currentVersion + 1);
setPageState(PageState.VIEW);
}
};
const handleOptimizeCode = () => {
// Здесь должен быть запрос к API для оптимизации кода через LLM
// В данном прототипе просто увеличиваем версию и добавляем фиктивные детали оптимизации
setCurrentVersion(currentVersion + 1);
setOptimizationDetails([
'Оптимизирована структура кода для улучшения читаемости',
'Улучшена обработка ошибок для повышения надежности',
'Добавлено кэширование для улучшения времени ответа'
]);
setPageState(PageState.VIEW_OPTIMIZED);
};
const handleEditCode = () => {
setPageState(PageState.EDIT);
};
// eslint-disable-next-line no-unused-vars
const handleCreateNewFunction = () => {
navigate('/optimization');
};
// Определяем, доступна ли кнопка сохранения
const isSaveButtonDisabled = () => {
if (pageState === PageState.CREATE) {
return !functionName || !code.trim();
} else if (pageState === PageState.EDIT) {
// Для Edit состояния нужно проверить, был ли изменен код
// В реальном приложении нужно сравнивать с оригинальным кодом
return !code.trim();
}
return true;
};
return (
<Container maxWidth="lg">
<Box sx={{ my: 4 }}>
{/* Заголовок и версия */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
{pageState === PageState.CREATE ? (
<TextField
label="Название"
variant="outlined"
fullWidth
value={functionName}
onChange={(e) => setFunctionName(e.target.value)}
placeholder="Новая функция"
sx={{ fontSize: '1.5rem' }}
/>
) : (
<Typography variant="h4" component="h1" gutterBottom>
{functionName}
</Typography>
)}
{pageState !== PageState.CREATE && (
<Box sx={{ ml: 3, display: 'flex', alignItems: 'center' }}>
<Typography variant="subtitle1" component="span" fontWeight="medium">
Версия:
</Typography>
<Typography variant="subtitle1" component="span" sx={{ ml: 1 }}>
{currentVersion}
</Typography>
</Box>
)}
</Box>
{/* Селекторы для языка и платформы (только в режиме создания) */}
{pageState === PageState.CREATE && (
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<FormControl sx={{ minWidth: 200 }}>
<InputLabel>Язык</InputLabel>
<Select
value={language}
label="Язык"
onChange={(e) => setLanguage(e.target.value)}
>
<MenuItem value="python">Python</MenuItem>
<MenuItem value="nodejs">Node.js</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ minWidth: 200 }}>
<InputLabel>Облачная платформа</InputLabel>
<Select
value={platform}
label="Облачная платформа"
onChange={(e) => setPlatform(e.target.value)}
>
<MenuItem value="yandex">Yandex Cloud Functions</MenuItem>
</Select>
</FormControl>
</Box>
)}
{/* Редактор кода */}
<Paper
elevation={3}
sx={{
p: 0,
mb: 3,
borderRadius: 1,
overflow: 'hidden',
height: '60vh',
display: 'flex',
flexDirection: 'column',
position: 'relative'
}}
>
<Box
ref={lineNumbersRef}
className="line-numbers"
sx={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '42px',
backgroundColor: '#f0f0f0',
borderRight: '1px solid #ddd',
overflowY: 'hidden',
zIndex: 1,
pt: '15px',
pb: '15px',
userSelect: 'none',
color: '#666',
textAlign: 'right',
fontSize: '14px',
fontFamily: '"JetBrains Mono", monospace',
lineHeight: 1.5
}}
>
{code.split('\n').map((_, i) => (
<div key={i} style={{ paddingRight: '8px' }}>{i + 1}</div>
))}
</Box>
<CodeEditor
ref={codeEditorRef}
value={code}
language={language === 'python' ? 'python' : 'js'}
onChange={(e) => {
if (pageState === PageState.CREATE || pageState === PageState.EDIT) {
setCode(e.target.value);
}
}}
onScroll={handleScroll}
padding={15}
data-color-mode="light"
style={{
fontSize: '14px',
fontFamily: '"JetBrains Mono", monospace',
flex: 1,
overflowY: 'auto',
backgroundColor: '#f5f5f5',
minHeight: '100%',
resize: 'none',
border: 'none',
outline: 'none',
paddingLeft: '50px',
// Блокировка редактирования в режимах просмотра
readOnly: pageState === PageState.VIEW || pageState === PageState.VIEW_OPTIMIZED,
}}
/>
</Paper>
{/* Блок с описанием оптимизаций (только для VIEW_OPTIMIZED) */}
{pageState === PageState.VIEW_OPTIMIZED && (
<Paper elevation={2} sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
Оптимизации
</Typography>
<ul>
{optimizationDetails.map((detail, index) => (
<li key={index}>
<Typography variant="body1">{detail}</Typography>
</li>
))}
</ul>
</Paper>
)}
{/* Кнопки действий в зависимости от состояния */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
{/* Кнопки для CREATE состояния */}
{pageState === PageState.CREATE && (
<Button
variant="contained"
color="primary"
onClick={handleSaveFunction}
disabled={isSaveButtonDisabled()}
>
Сохранить
</Button>
)}
{/* Кнопки для VIEW состояния */}
{pageState === PageState.VIEW && (
<>
<Button
variant="outlined"
onClick={handleEditCode}
>
Редактировать
</Button>
<Button
variant="contained"
color="primary"
onClick={handleOptimizeCode}
>
Оптимизировать
</Button>
</>
)}
{/* Кнопки для VIEW_OPTIMIZED состояния */}
{pageState === PageState.VIEW_OPTIMIZED && (
<Button
variant="outlined"
onClick={handleEditCode}
>
Редактировать
</Button>
)}
{/* Кнопки для EDIT состояния */}
{pageState === PageState.EDIT && (
<Button
variant="contained"
color="primary"
onClick={handleSaveFunction}
disabled={isSaveButtonDisabled()}
>
Сохранить
</Button>
)}
</Box>
{/* Предупреждение об успешном сохранении */}
{alertVisible && (
<Alert
severity="success"
sx={{
position: 'fixed',
bottom: 16,
right: 16,
zIndex: 9999
}}
>
Функция успешно сохранена
</Alert>
)}
</Box>
</Container>
);
}
export default OptimizationPage;
\ No newline at end of file
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
Alert,
AlertTitle,
Box, Box,
Button, Button,
Card, Card,
CardContent, CardContent,
CircularProgress,
Container, Container,
FormControl, FormControl,
Grid, Grid,
InputLabel, InputLabel,
ListSubheader,
MenuItem, MenuItem,
Paper, Paper,
Select, Select,
Tab, Tab,
Tabs, Tabs,
Typography, Typography,
useTheme,
} from '@mui/material'; } from '@mui/material';
import FileDownloadIcon from '@mui/icons-material/FileDownload'; import CodeIcon from '@mui/icons-material/Code';
import { Line, Bar } from 'react-chartjs-2'; import JavascriptIcon from '@mui/icons-material/Javascript';
import { Line } from 'react-chartjs-2';
import { import {
Chart as ChartJS, Chart as ChartJS,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
BarElement,
Title, Title,
Tooltip, Tooltip as ChartTooltip,
Legend, Legend,
} from 'chart.js'; } from 'chart.js';
...@@ -35,290 +40,811 @@ ChartJS.register( ...@@ -35,290 +40,811 @@ ChartJS.register(
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
BarElement,
Title, Title,
Tooltip, ChartTooltip,
Legend Legend
); );
// Данные о времени отклика (мс) // Моковые данные для тестовых профилей
const responseTimeData = { const mockProfiles = [
labels: ['0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100'], {
datasets: [ functionId: 'func1',
{ functionName: 'API Gateway Handler',
label: 'До оптимизации', language: 'python',
data: [165, 170, 172, 168, 182, 190, 178, 175, 172, 178, 185], versions: [
borderColor: '#f44336', {
backgroundColor: 'rgba(244, 67, 54, 0.1)', versionId: 1,
tension: 0.4, versionLabel: 'Версия 1',
}, tests: [
{ {
label: 'После оптимизации', id: 'test1',
data: [132, 128, 125, 130, 133, 140, 138, 135, 130, 128, 124], date: '2024-06-14 10:30',
borderColor: '#4caf50', runtime: 'python310',
backgroundColor: 'rgba(76, 175, 80, 0.1)', platform: 'Яндекс Облако',
tension: 0.4, iterations: 100,
}, concurrency: 10,
], metrics: {
}; coldStart: {
p25: 250,
// Данные об использовании ресурсов p50: 280,
const resourceUsageData = { p75: 310,
labels: ['ЦП', 'Память', 'Сеть', 'Хранилище'], p100: 350
datasets: [ },
{ executionTime: {
label: 'До оптимизации', p25: 120,
data: [48, 320, 75, 55], p50: 135,
backgroundColor: 'rgba(244, 67, 54, 0.7)', p75: 150,
}, p100: 180
{ },
label: 'После оптимизации', cpu: {
data: [32, 256, 60, 45], p25: 20,
backgroundColor: 'rgba(76, 175, 80, 0.7)', p50: 25,
}, p75: 32,
], p100: 45
}; },
memory: {
p25: 180,
p50: 230,
p75: 260,
p100: 300
},
network: {
p25: 15,
p50: 22,
p75: 30,
p100: 45
}
}
},
{
id: 'test2',
date: '2024-06-15 14:45',
runtime: 'python310',
platform: 'Яндекс Облако',
iterations: 150,
concurrency: 15,
metrics: {
coldStart: {
p25: 245,
p50: 275,
p75: 305,
p100: 345
},
executionTime: {
p25: 115,
p50: 130,
p75: 145,
p100: 175
},
cpu: {
p25: 18,
p50: 23,
p75: 30,
p100: 42
},
memory: {
p25: 175,
p50: 225,
p75: 255,
p100: 295
},
network: {
p25: 14,
p50: 20,
p75: 27,
p100: 42
}
}
}
]
},
{
versionId: 2,
versionLabel: 'Версия 2',
tests: [
{
id: 'test3',
date: '2024-06-16 09:15',
runtime: 'python310',
platform: 'Яндекс Облако',
iterations: 100,
concurrency: 10,
metrics: {
coldStart: {
p25: 220,
p50: 245,
p75: 275,
p100: 320
},
executionTime: {
p25: 105,
p50: 120,
p75: 135,
p100: 165
},
cpu: {
p25: 15,
p50: 20,
p75: 27,
p100: 38
},
memory: {
p25: 160,
p50: 200,
p75: 235,
p100: 275
},
network: {
p25: 12,
p50: 18,
p75: 25,
p100: 38
}
}
}
]
}
]
},
{
functionId: 'func2',
functionName: 'Database Worker',
language: 'nodejs',
versions: [
{
versionId: 1,
versionLabel: 'Версия 1',
tests: [
{
id: 'test4',
date: '2024-06-14 15:30',
runtime: 'nodejs16',
platform: 'Яндекс Облако',
iterations: 100,
concurrency: 8,
metrics: {
coldStart: {
p25: 260,
p50: 290,
p75: 320,
p100: 360
},
executionTime: {
p25: 130,
p50: 145,
p75: 160,
p100: 190
},
cpu: {
p25: 22,
p50: 28,
p75: 35,
p100: 50
},
memory: {
p25: 195,
p50: 245,
p75: 275,
p100: 320
},
network: {
p25: 18,
p50: 25,
p75: 33,
p100: 48
}
}
}
]
}
]
}
];
// Данные о холодном старте (мс) // Состояния страницы
const coldStartData = { const PAGE_STATES = {
labels: ['API Gateway', 'Обработка данных', 'Работа с БД', 'Обработка изображений'], SELECTION: 'selection',
datasets: [ VIEW: 'view',
{ COMPARISON: 'comparison',
label: 'До оптимизации',
data: [420, 380, 350, 480],
backgroundColor: 'rgba(244, 67, 54, 0.7)',
},
{
label: 'После оптимизации',
data: [320, 280, 260, 350],
backgroundColor: 'rgba(76, 175, 80, 0.7)',
},
],
}; };
// Данные о пропускной способности (запросов/сек) // Метрики для отображения
const throughputData = { const METRICS = [
labels: ['0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100'], { id: 'coldStart', label: 'Время холодного старта (мс)' },
datasets: [ { id: 'executionTime', label: 'Время выполнения (мс)' },
{ { id: 'cpu', label: 'Потребление ЦП (%)' },
label: 'До оптимизации', { id: 'memory', label: 'Потребление памяти (МБ)' },
data: [60, 62, 58, 55, 57, 52, 50, 48, 45, 43, 40], { id: 'network', label: 'Потребление сетевого трафика (КБ/с)' },
borderColor: '#f44336', ];
backgroundColor: 'rgba(244, 67, 54, 0.1)',
tension: 0.4,
},
{
label: 'После оптимизации',
data: [70, 75, 78, 82, 85, 83, 80, 78, 75, 72, 70],
borderColor: '#4caf50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
tension: 0.4,
},
],
};
// Общие опции для графиков // Общие опции для графиков
const chartOptions = { const getChartOptions = (title) => ({
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
position: 'top', position: 'top',
}, },
title: {
display: true,
text: title,
},
}, },
}; });
function ResultsPage() { function ResultsPage() {
const [tabValue, setTabValue] = useState(0); const theme = useTheme();
const [platform, setPlatform] = useState('yandex'); const [pageState, setPageState] = useState(PAGE_STATES.SELECTION);
const [timeRange, setTimeRange] = useState('week'); const [profiles, setProfiles] = useState(mockProfiles);
const [selectedProfile1, setSelectedProfile1] = useState('');
const [selectedProfile2, setSelectedProfile2] = useState('');
const [currentTabIndex, setCurrentTabIndex] = useState(0);
const [profileData1, setProfileData1] = useState(null);
const [profileData2, setProfileData2] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [compatibleProfiles, setCompatibleProfiles] = useState([]);
// Обработка изменения вкладки метрики
const handleTabChange = (event, newValue) => { const handleTabChange = (event, newValue) => {
setTabValue(newValue); setCurrentTabIndex(newValue);
};
// Обработка выбора первого профиля
const handleProfile1Change = (event) => {
const profile = event.target.value;
setSelectedProfile1(profile);
setSelectedProfile2('');
if (profile) {
// Поиск данных профиля
const [funcId, verId, testId] = profile.split(':');
// Поиск совместимых профилей (той же функции, но более новых версий)
const selectedFunction = profiles.find(f => f.functionId === funcId);
if (selectedFunction) {
const selectedVersionIndex = selectedFunction.versions.findIndex(v => v.versionId.toString() === verId);
if (selectedVersionIndex !== -1) {
// Собираем все профили тестов более новых версий
const compatibleOptions = [];
for (let i = selectedVersionIndex + 1; i < selectedFunction.versions.length; i++) {
const version = selectedFunction.versions[i];
version.tests.forEach(test => {
compatibleOptions.push({
id: `${funcId}:${version.versionId}:${test.id}`,
functionName: selectedFunction.functionName,
versionLabel: version.versionLabel,
testDate: test.date
});
});
}
setCompatibleProfiles(compatibleOptions);
}
}
// Загрузка данных профиля
loadProfileData(profile, setProfileData1);
setPageState(PAGE_STATES.VIEW);
} else {
setProfileData1(null);
setPageState(PAGE_STATES.SELECTION);
}
};
// Обработка выбора второго профиля
const handleProfile2Change = (event) => {
const profile = event.target.value;
setSelectedProfile2(profile);
if (profile) {
// Загрузка данных профиля
loadProfileData(profile, setProfileData2);
setPageState(PAGE_STATES.COMPARISON);
} else {
setProfileData2(null);
setPageState(PAGE_STATES.VIEW);
}
};
// Загрузка данных профиля
const loadProfileData = (profileId, setData) => {
if (!profileId) return;
setLoading(true);
setError(null);
try {
// Парсинг ID профиля
const [funcId, versionId, testId] = profileId.split(':');
// Поиск данных в моковом наборе
const selectedFunction = profiles.find(f => f.functionId === funcId);
if (selectedFunction) {
const selectedVersion = selectedFunction.versions.find(v => v.versionId.toString() === versionId);
if (selectedVersion) {
const selectedTest = selectedVersion.tests.find(t => t.id === testId);
if (selectedTest) {
const profileData = {
functionId: funcId,
functionName: selectedFunction.functionName,
language: selectedFunction.language,
versionId: versionId,
versionLabel: selectedVersion.versionLabel,
...selectedTest
};
// Имитация асинхронной загрузки
setTimeout(() => {
setData(profileData);
setLoading(false);
}, 500);
return;
}
}
}
throw new Error('Профиль не найден');
} catch (err) {
console.error('Error loading profile:', err);
setError('Не удалось загрузить данные профиля');
setLoading(false);
}
};
// Получение данных для метрик
const getMetricChartData = (metric) => {
const isComparisonMode = pageState === PAGE_STATES.COMPARISON && profileData2;
// Проверка наличия данных для метрики
if (!profileData1?.metrics[metric]) {
return null;
}
const labels = ['p25', 'p50', 'p75', 'p100'];
const datasets = [
{
label: `${profileData1.functionName} ${profileData1.versionLabel}`,
data: [
profileData1.metrics[metric].p25,
profileData1.metrics[metric].p50,
profileData1.metrics[metric].p75,
profileData1.metrics[metric].p100
],
borderColor: theme.palette.primary.main,
backgroundColor: `${theme.palette.primary.main}20`,
tension: 0.3,
}
];
if (isComparisonMode && profileData2?.metrics[metric]) {
datasets.push({
label: `${profileData2.functionName} ${profileData2.versionLabel}`,
data: [
profileData2.metrics[metric].p25,
profileData2.metrics[metric].p50,
profileData2.metrics[metric].p75,
profileData2.metrics[metric].p100
],
borderColor: theme.palette.secondary.main,
backgroundColor: `${theme.palette.secondary.main}20`,
tension: 0.3,
});
}
return {
labels: labels.map(p => p.toUpperCase().replace('P', '')),
datasets
};
};
// Построение древовидного списка профилей для селектора
const renderProfilesMenu = () => {
if (profiles.length === 0) {
return [
<MenuItem key="empty" disabled>
Нет сохранённых результатов
</MenuItem>
];
}
return [
<MenuItem key="default" value="" divider>
Выберите профиль
</MenuItem>,
...profiles.map(func => [
<ListSubheader key={`func-${func.functionId}`} sx={{ display: 'flex', alignItems: 'center' }}>
{func.language === 'python' ?
<CodeIcon fontSize="small" color="primary" sx={{ mr: 1 }} /> :
<JavascriptIcon fontSize="small" color="warning" sx={{ mr: 1 }} />}
{func.functionName}
</ListSubheader>,
...func.versions.flatMap(ver => [
<MenuItem
key={`ver-${func.functionId}-${ver.versionId}`}
sx={{ pl: 4 }}
disabled
>
{ver.versionLabel}
</MenuItem>,
...ver.tests.map(test => (
<MenuItem
key={`test-${func.functionId}-${ver.versionId}-${test.id}`}
value={`${func.functionId}:${ver.versionId}:${test.id}`}
sx={{ pl: 6 }}
>
Тест {test.date}
</MenuItem>
))
])
]).flat()
];
}; };
// Рендеринг списка совместимых профилей для сравнения
const renderCompatibleProfilesMenu = () => {
if (compatibleProfiles.length === 0) {
return [
<MenuItem key="empty" disabled>
Нет профилей для сравнения
</MenuItem>
];
}
return [
<MenuItem key="default" value="">
Выберите профиль
</MenuItem>,
...compatibleProfiles.map(profile => (
<MenuItem
key={profile.id}
value={profile.id}
>
{profile.versionLabel} - Тест {profile.testDate}
</MenuItem>
))
];
};
// Расчет процентной разницы между метриками
const calculateDiff = (metric) => {
if (!profileData1 || !profileData2 || !profileData1.metrics[metric] || !profileData2.metrics[metric]) {
return null;
}
const p50Value1 = profileData1.metrics[metric].p50;
const p50Value2 = profileData2.metrics[metric].p50;
if (p50Value1 === 0) return null;
const diff = ((p50Value2 - p50Value1) / p50Value1) * 100;
// Определить, является ли изменение улучшением
let isImprovement;
if (metric === 'executionTime' || metric === 'coldStart' || metric === 'cpu' ||
metric === 'memory' || metric === 'network') {
// Для этих метрик уменьшение - это улучшение
isImprovement = diff < 0;
} else {
// Для других метрик увеличение может быть улучшением
isImprovement = diff > 0;
}
return {
value: diff.toFixed(1),
isImprovement
};
};
// Получение иконки языка
const getLanguageIcon = (language) => {
switch (language) {
case 'python':
return <CodeIcon fontSize="small" color="primary" />;
case 'nodejs':
return <JavascriptIcon fontSize="small" color="warning" />;
default:
return <CodeIcon fontSize="small" />;
}
};
// Проверка ошибок загрузки и пустых данных
useEffect(() => {
if (profiles.length === 0) {
setError('Нет сохранённых результатов. Запустите тестирование');
} else {
setError(null);
}
}, [profiles]);
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
<Box sx={{ my: 4 }}> <Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom> <Typography variant="h4" component="h1" gutterBottom>
Результаты тестирования Результаты тестирования
</Typography> </Typography>
<Typography variant="body1" paragraph>
Сравнительный анализ производительности serverless-функций до и после оптимизации.
</Typography>
<Grid container spacing={3} sx={{ mb: 4 }}> <Grid container spacing={2} sx={{ mb: 4 }}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={5}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Облачная платформа</InputLabel> <InputLabel>Профиль 1</InputLabel>
<Select <Select
value={platform} value={selectedProfile1}
label="Облачная платформа" label="Профиль 1"
onChange={(e) => setPlatform(e.target.value)} onChange={handleProfile1Change}
MenuProps={{ style: { maxHeight: 500 } }}
> >
<MenuItem value="yandex">Яндекс Облако</MenuItem> {renderProfilesMenu()}
<MenuItem value="sber">SberCloud</MenuItem>
<MenuItem value="mail">Mail.ru Cloud Solutions</MenuItem>
<MenuItem value="selectel">Selectel</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth> <Grid item xs={12} md={2} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<InputLabel>Временной диапазон</InputLabel> <Typography variant="body1" sx={{ color: 'text.secondary' }}>
сравнить с
</Typography>
</Grid>
<Grid item xs={12} md={5}>
<FormControl fullWidth disabled={!selectedProfile1}>
<InputLabel>Профиль 2</InputLabel>
<Select <Select
value={timeRange} value={selectedProfile2}
label="Временной диапазон" label="Профиль 2"
onChange={(e) => setTimeRange(e.target.value)} onChange={handleProfile2Change}
MenuProps={{ style: { maxHeight: 500 } }}
> >
<MenuItem value="day">Последний день</MenuItem> {renderCompatibleProfilesMenu()}
<MenuItem value="week">Последняя неделя</MenuItem>
<MenuItem value="month">Последний месяц</MenuItem>
<MenuItem value="custom">Пользовательский</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
</Grid> </Grid>
<Paper sx={{ mb: 4 }}> {pageState === PAGE_STATES.SELECTION && (
<Tabs <Box sx={{ textAlign: 'center', py: 8 }}>
value={tabValue} {error ? (
onChange={handleTabChange} <Alert severity="info" sx={{ display: 'inline-flex' }}>
sx={{ borderBottom: 1, borderColor: 'divider' }} <AlertTitle>Информация</AlertTitle>
centered {error}
> </Alert>
<Tab label="Время отклика" /> ) : (
<Tab label="Использование ресурсов" /> <Typography variant="h6" color="text.secondary">
<Tab label="Холодный старт" /> Выберите профиль для просмотра результатов тестирования
<Tab label="Пропускная способность" /> </Typography>
</Tabs>
<Box sx={{ p: 3 }}>
{tabValue === 0 && (
<Box>
<Typography variant="h6" gutterBottom>
Время отклика (мс) при различных уровнях нагрузки
</Typography>
<Typography variant="body2" paragraph>
График показывает сравнение времени отклика до и после оптимизации при увеличении
нагрузки от 0 до 100%. Наблюдается снижение времени отклика на 23% после оптимизации.
</Typography>
<Box sx={{ height: 400 }}>
<Line options={chartOptions} data={responseTimeData} />
</Box>
</Box>
)}
{tabValue === 1 && (
<Box>
<Typography variant="h6" gutterBottom>
Использование ресурсов
</Typography>
<Typography variant="body2" paragraph>
График сравнивает использование различных ресурсов до и после оптимизации.
Значения для ЦП указаны в процентах, для памяти - в МБ, для сети и хранилища - в МБ/сек.
</Typography>
<Box sx={{ height: 400 }}>
<Bar options={chartOptions} data={resourceUsageData} />
</Box>
</Box>
)}
{tabValue === 2 && (
<Box>
<Typography variant="h6" gutterBottom>
Время холодного старта (мс)
</Typography>
<Typography variant="body2" paragraph>
График показывает время холодного старта для различных типов функций до и после оптимизации.
Среднее снижение составило 27%.
</Typography>
<Box sx={{ height: 400 }}>
<Bar options={chartOptions} data={coldStartData} />
</Box>
</Box>
)}
{tabValue === 3 && (
<Box>
<Typography variant="h6" gutterBottom>
Пропускная способность (запросов/сек)
</Typography>
<Typography variant="body2" paragraph>
График показывает сравнение пропускной способности до и после оптимизации при увеличении
нагрузки от 0 до 100%. Наблюдается увеличение пропускной способности на 65%.
</Typography>
<Box sx={{ height: 400 }}>
<Line options={chartOptions} data={throughputData} />
</Box>
</Box>
)} )}
</Box> </Box>
</Paper> )}
<Grid container spacing={3}> {(pageState === PAGE_STATES.VIEW || pageState === PAGE_STATES.COMPARISON) && (
<Grid item xs={12} md={6} sx={{ display: 'flex' }}> <>
<Card sx={{ width: '100%', height: '100%' }}> {loading ? (
<CardContent sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<Typography variant="h6" gutterBottom> <CircularProgress />
Сводка улучшений </Box>
</Typography> ) : error ? (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}> <Alert
<Typography variant="body2">Время отклика:</Typography> severity="error"
<Typography variant="body2" color="success.main">-23%</Typography> action={
</Box> <Button color="inherit" size="small" onClick={() => loadProfileData(selectedProfile1, setProfileData1)}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}> Повторить
<Typography variant="body2">Использование ЦП:</Typography> </Button>
<Typography variant="body2" color="success.main">-33%</Typography> }
</Box> >
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}> <AlertTitle>Ошибка</AlertTitle>
<Typography variant="body2">Использование памяти:</Typography> {error}
<Typography variant="body2" color="success.main">-20%</Typography> </Alert>
</Box> ) : profileData1 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}> <>
<Typography variant="body2">Время холодного старта:</Typography> <Paper sx={{ mb: 4 }}>
<Typography variant="body2" color="success.main">-27%</Typography> <Tabs
</Box> value={currentTabIndex}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> onChange={handleTabChange}
<Typography variant="body2">Пропускная способность:</Typography> sx={{ borderBottom: 1, borderColor: 'divider' }}
<Typography variant="body2" color="success.main">+65%</Typography> variant="scrollable"
</Box> scrollButtons="auto"
<Box sx={{ flexGrow: 1 }} /> >
</CardContent> {METRICS.map((metric, index) => {
</Card> const hasData = Boolean(profileData1.metrics[metric.id]);
</Grid> return (
<Grid item xs={12} md={6} sx={{ display: 'flex' }}> <Tab
<Card sx={{ width: '100%', height: '100%' }}> key={metric.id}
<CardContent sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> label={metric.label}
<Typography variant="h6" gutterBottom> disabled={!hasData}
Детали теста sx={{ opacity: hasData ? 1 : 0.5 }}
</Typography> />
<Typography variant="body2"> );
<strong>Платформа:</strong> Яндекс Облако })}
</Typography> </Tabs>
<Typography variant="body2"> <Box sx={{ p: 3 }}>
<strong>Сценарий:</strong> API Gateway {METRICS.map((metric, index) => {
</Typography> if (currentTabIndex !== index) return null;
<Typography variant="body2">
<strong>Дата запуска:</strong> 12.05.2024 const chartData = getMetricChartData(metric.id);
</Typography> if (!chartData) {
<Typography variant="body2"> return (
<strong>Кол-во итераций:</strong> 100 <Box key={metric.id} sx={{ textAlign: 'center', py: 4 }}>
</Typography> <Typography color="text.secondary">
<Typography variant="body2"> Нет данных для этой метрики
<strong>Одновременных запросов:</strong> 10 </Typography>
</Typography> </Box>
<Typography variant="body2"> );
<strong>Примененные оптимизации:</strong> Кэширование, оптимизация пакета зависимостей, предварительный разогрев }
</Typography>
<Box sx={{ flexGrow: 1 }} /> return (
<Button <Box key={metric.id}>
variant="outlined" <Box sx={{ height: 400 }}>
startIcon={<FileDownloadIcon />} <Line
sx={{ mt: 2 }} options={getChartOptions(`${metric.label} (перцентили)`)}
> data={chartData}
Экспорт отчета />
</Button> </Box>
</CardContent> </Box>
</Card> );
</Grid> })}
</Grid> </Box>
</Paper>
<Grid container spacing={3}>
{pageState === PAGE_STATES.COMPARISON ? (
<>
<Grid item xs={12} md={4} sx={{ display: 'flex' }}>
<Card sx={{ width: '100%', height: '100%' }}>
<CardContent sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Typography variant="h6" gutterBottom>
Детали теста 1
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
{getLanguageIcon(profileData1.language)}
<Box sx={{ ml: 1 }}>
<Typography variant="body2">
<strong>Функция:</strong> {profileData1.functionName}
</Typography>
</Box>
</Box>
<Typography variant="body2">
<strong>Версия:</strong> {profileData1.versionLabel}
</Typography>
<Typography variant="body2">
<strong>Платформа:</strong> {profileData1.platform}
</Typography>
<Typography variant="body2">
<strong>Рантайм:</strong> {profileData1.runtime}
</Typography>
<Typography variant="body2">
<strong>Дата запуска:</strong> {profileData1.date}
</Typography>
<Typography variant="body2">
<strong>Кол-во итераций:</strong> {profileData1.iterations}
</Typography>
<Typography variant="body2">
<strong>Параллельных запусков:</strong> {profileData1.concurrency}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={4} sx={{ display: 'flex' }}>
<Card sx={{ width: '100%', height: '100%' }}>
<CardContent sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Typography variant="h6" gutterBottom>
Детали теста 2
</Typography>
{profileData2 && (
<>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
{getLanguageIcon(profileData2.language)}
<Box sx={{ ml: 1 }}>
<Typography variant="body2">
<strong>Функция:</strong> {profileData2.functionName}
</Typography>
</Box>
</Box>
<Typography variant="body2">
<strong>Версия:</strong> {profileData2.versionLabel}
</Typography>
<Typography variant="body2">
<strong>Платформа:</strong> {profileData2.platform}
</Typography>
<Typography variant="body2">
<strong>Рантайм:</strong> {profileData2.runtime}
</Typography>
<Typography variant="body2">
<strong>Дата запуска:</strong> {profileData2.date}
</Typography>
<Typography variant="body2">
<strong>Кол-во итераций:</strong> {profileData2.iterations}
</Typography>
<Typography variant="body2">
<strong>Параллельных запусков:</strong> {profileData2.concurrency}
</Typography>
</>
)}
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={4} sx={{ display: 'flex' }}>
<Card sx={{ width: '100%', height: '100%' }}>
<CardContent sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Typography variant="h6" gutterBottom>
Сводка изменений
</Typography>
{profileData1 && profileData2 && (
<>
{METRICS.map(metric => {
const diff = calculateDiff(metric.id);
if (!diff) return null;
const color = diff.isImprovement ? 'success.main' : 'error.main';
const prefix = diff.value.startsWith('-') ? '' : '+';
return (
<Box key={metric.id} sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2">{metric.label}:</Typography>
<Typography variant="body2" color={color}>
{prefix}{diff.value}%
</Typography>
</Box>
);
})}
<Box sx={{ flexGrow: 1 }} />
</>
)}
</CardContent>
</Card>
</Grid>
</>
) : (
<Grid item xs={12} sx={{ display: 'flex' }}>
<Card sx={{ width: '100%' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Детали теста
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
{getLanguageIcon(profileData1.language)}
<Box sx={{ ml: 1 }}>
<Typography variant="body2">
<strong>Функция:</strong> {profileData1.functionName}
</Typography>
</Box>
</Box>
<Typography variant="body2">
<strong>Версия:</strong> {profileData1.versionLabel}
</Typography>
<Typography variant="body2">
<strong>Платформа:</strong> {profileData1.platform}
</Typography>
<Typography variant="body2">
<strong>Рантайм:</strong> {profileData1.runtime}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2">
<strong>Дата запуска:</strong> {profileData1.date}
</Typography>
<Typography variant="body2">
<strong>Кол-во итераций:</strong> {profileData1.iterations}
</Typography>
<Typography variant="body2">
<strong>Параллельных запусков:</strong> {profileData1.concurrency}
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
</Grid>
)}
</Grid>
</>
)}
</>
)}
</Box> </Box>
</Container> </Container>
); );
......
import React, { useState } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { import {
Box, Box,
Button, Button,
...@@ -6,6 +7,11 @@ import { ...@@ -6,6 +7,11 @@ import {
CardContent, CardContent,
Chip, Chip,
Container, Container,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider, Divider,
FormControl, FormControl,
Grid, Grid,
...@@ -28,8 +34,10 @@ import CodeIcon from '@mui/icons-material/Code'; ...@@ -28,8 +34,10 @@ import CodeIcon from '@mui/icons-material/Code';
import CloudIcon from '@mui/icons-material/Cloud'; import CloudIcon from '@mui/icons-material/Cloud';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import BugReportIcon from '@mui/icons-material/BugReport'; import BugReportIcon from '@mui/icons-material/BugReport';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
const testSteps = ['Настройка теста', 'Запуск тестов', 'Анализ результатов', 'Формирование рекомендаций']; const testSteps = ['Конфигурация', 'Тестирование', 'Результаты'];
const cloudOptions = [ const cloudOptions = [
{ value: 'yandex', label: 'Яндекс Облако' }, { value: 'yandex', label: 'Яндекс Облако' },
...@@ -46,39 +54,208 @@ const scenarioOptions = [ ...@@ -46,39 +54,208 @@ const scenarioOptions = [
{ value: 'custom', label: 'Пользовательский сценарий' }, { value: 'custom', label: 'Пользовательский сценарий' },
]; ];
// Моковые данные для выбора функции
const mockFunctions = [
{
id: 'func1',
name: 'API Gateway Handler',
versions: [
{ version: 1, label: 'Версия 1' },
{ version: 2, label: 'Версия 2' },
{ version: 3, label: 'Версия 3' }
]
},
{
id: 'func2',
name: 'Database Worker',
versions: [
{ version: 1, label: 'Версия 1' },
{ version: 2, label: 'Версия 2' }
]
},
{
id: 'func3',
name: 'Image Processor',
versions: [
{ version: 1, label: 'Версия 1' }
]
}
];
// Моковые данные для рантаймов
const runtimeOptions = [
{ value: 'python310', label: 'Python 3.10' },
{ value: 'python39', label: 'Python 3.9' },
{ value: 'nodejs16', label: 'Node.js 16' },
{ value: 'nodejs14', label: 'Node.js 14' },
{ value: 'go119', label: 'Go 1.19' }
];
function TestingPage() { function TestingPage() {
const navigate = useNavigate();
const params = useParams();
const { functionId, version } = params;
// Состояния
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [cloud, setCloud] = useState('yandex');
const [scenario, setScenario] = useState('api');
const [iterations, setIterations] = useState(100); const [iterations, setIterations] = useState(100);
const [concurrency, setConcurrency] = useState(10); const [parallelRuns, setParallelRuns] = useState(10);
const [optimizationEnabled, setOptimizationEnabled] = useState(true); const [timeout, setTimeout] = useState(5000);
const [testRunning, setTestRunning] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [testStartTime, setTestStartTime] = useState(null);
const [testDuration, setTestDuration] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedFunction, setSelectedFunction] = useState(functionId || '');
const [selectedVersion, setSelectedVersion] = useState(version ? parseInt(version) : '');
const [selectedRuntime, setSelectedRuntime] = useState('');
const [versions, setVersions] = useState([]);
// Для таймера
const timerRef = useRef(null);
const [elapsedTime, setElapsedTime] = useState(0);
// Проверка валидности формы
const isFormValid = selectedFunction && selectedVersion && selectedRuntime &&
iterations > 0 && parallelRuns > 0 && timeout > 0;
// Эффект для инициализации выбранной функции и версии из URL
useEffect(() => {
if (functionId) {
const func = mockFunctions.find(f => f.id === functionId);
if (func) {
setSelectedFunction(functionId);
setVersions(func.versions);
if (version) {
const versionInt = parseInt(version);
const versionExists = func.versions.some(v => v.version === versionInt);
if (versionExists) {
setSelectedVersion(versionInt);
}
}
}
}
}, [functionId, version]);
// Эффект для обновления доступных версий при выборе функции
useEffect(() => {
if (selectedFunction) {
const func = mockFunctions.find(f => f.id === selectedFunction);
if (func) {
setVersions(func.versions);
if (func.versions.length > 0 && !selectedVersion) {
setSelectedVersion(func.versions[0].version);
}
}
} else {
setVersions([]);
setSelectedVersion('');
}
}, [selectedFunction]);
// Эффект для таймера
useEffect(() => {
if (activeStep === 1 && testStartTime) {
timerRef.current = setInterval(() => {
const elapsed = Math.floor((Date.now() - testStartTime) / 1000);
setElapsedTime(elapsed);
}, 1000);
return () => clearInterval(timerRef.current);
}
}, [activeStep, testStartTime]);
// Эффект для симуляции прогресса тестирования
useEffect(() => {
if (activeStep === 1) {
const interval = setInterval(() => {
setProgress(prevProgress => {
const newProgress = prevProgress + 1;
if (newProgress >= 100) {
clearInterval(interval);
// Тестирование завершено
const duration = Math.floor((Date.now() - testStartTime) / 1000);
setTestDuration(duration);
setActiveStep(2);
clearInterval(timerRef.current);
return 100;
}
return newProgress;
});
}, 150);
return () => clearInterval(interval);
}
}, [activeStep, testStartTime]);
// Обработчики
const handleStartTest = () => { const handleStartTest = () => {
setTestRunning(true);
setActiveStep(1); setActiveStep(1);
setProgress(0);
// Симуляция прогресса setTestStartTime(Date.now());
const interval = setInterval(() => { setElapsedTime(0);
setProgress((prevProgress) => {
const newProgress = prevProgress + 5;
if (newProgress >= 100) {
clearInterval(interval);
setTestRunning(false);
setActiveStep(2);
return 100;
}
return newProgress;
});
}, 500);
}; };
const handleReset = () => { const handleReset = () => {
setActiveStep(0); setActiveStep(0);
setProgress(0); setProgress(0);
setTestRunning(false); };
const handleFunctionChange = (event) => {
setSelectedFunction(event.target.value);
};
const handleVersionChange = (event) => {
setSelectedVersion(event.target.value);
};
const handleRuntimeChange = (event) => {
setSelectedRuntime(event.target.value);
};
const handleCancelTest = () => {
setIsDialogOpen(true);
};
const handleConfirmCancel = () => {
clearInterval(timerRef.current);
setIsDialogOpen(false);
setActiveStep(0);
setProgress(0);
setTestStartTime(null);
};
const handleDialogClose = () => {
setIsDialogOpen(false);
};
const handleViewResults = () => {
navigate(`/results?testId=${selectedFunction}-${selectedVersion}`);
};
const handleNewTest = () => {
setActiveStep(0);
setProgress(0);
setTestStartTime(null);
setTestDuration(null);
};
// Форматирование времени
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// Получение названия выбранной функции и версии
const getSelectedFunctionName = () => {
if (selectedFunction && selectedVersion) {
const func = mockFunctions.find(f => f.id === selectedFunction);
if (func) {
return `${func.name} v${selectedVersion}`;
}
}
return '';
}; };
return ( return (
...@@ -103,119 +280,95 @@ function TestingPage() { ...@@ -103,119 +280,95 @@ function TestingPage() {
{activeStep === 0 && ( {activeStep === 0 && (
<Box> <Box>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Параметры тестирования Настройки тестирования
</Typography> </Typography>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel>Облачная платформа</InputLabel> <InputLabel>Функция</InputLabel>
<Select <Select
value={cloud} value={selectedFunction}
label="Облачная платформа" label="Функция"
onChange={(e) => setCloud(e.target.value)} onChange={handleFunctionChange}
> >
{cloudOptions.map((option) => ( {mockFunctions.map((func) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={func.id} value={func.id}>
{option.label} {func.name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth margin="normal" disabled={!selectedFunction}>
<InputLabel>Версия</InputLabel>
<Select
value={selectedVersion}
label="Версия"
onChange={handleVersionChange}
>
{versions.map((ver) => (
<MenuItem key={ver.version} value={ver.version}>
{ver.label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel>Сценарий тестирования</InputLabel> <InputLabel>Рантайм</InputLabel>
<Select <Select
value={scenario} value={selectedRuntime}
label="Сценарий тестирования" label="Рантайм"
onChange={(e) => setScenario(e.target.value)} onChange={handleRuntimeChange}
> >
{scenarioOptions.map((option) => ( {runtimeOptions.map((option) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
{option.label} {option.label}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid item xs={6}>
<TextField
fullWidth
label="Кол-во итераций"
type="number"
value={iterations}
onChange={(e) => setIterations(Number(e.target.value))}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="Одновременные запросы"
type="number"
value={concurrency}
onChange={(e) => setConcurrency(Number(e.target.value))}
/>
</Grid>
</Grid>
<FormControlLabel
control={
<Switch
checked={optimizationEnabled}
onChange={(e) => setOptimizationEnabled(e.target.checked)}
/>
}
label="Применить оптимизацию"
sx={{ mt: 2 }}
/>
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Card sx={{ mb: 2, bgcolor: '#f5f5f5' }}> <TextField
<CardContent> fullWidth
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }}> margin="normal"
<CloudIcon color="primary" /> label="Количество итераций"
<Typography variant="subtitle1"> type="number"
Информация о платформе value={iterations}
</Typography> onChange={(e) => setIterations(Number(e.target.value))}
</Stack> inputProps={{ min: 1 }}
<Typography variant="body2"> />
Яндекс Облако - российская облачная платформа, предоставляющая
serverless-решения на базе отечественных технологий. <TextField
</Typography> fullWidth
<Divider sx={{ my: 2 }} /> margin="normal"
<Stack direction="row" spacing={1}> label="Параллельные запуски"
<Chip label="4 региона" size="small" /> type="number"
<Chip label="Python 3.10" size="small" /> value={parallelRuns}
<Chip label="Node.js 16" size="small" /> onChange={(e) => setParallelRuns(Number(e.target.value))}
</Stack> inputProps={{ min: 1 }}
</CardContent> />
</Card>
<TextField
<Card sx={{ bgcolor: '#f5f5f5' }}> fullWidth
<CardContent> margin="normal"
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }}> label="Таймаут (мс)"
<CodeIcon color="primary" /> type="number"
<Typography variant="subtitle1"> value={timeout}
Сценарий тестирования onChange={(e) => setTimeout(Number(e.target.value))}
</Typography> inputProps={{ min: 100 }}
</Stack> />
<Typography variant="body2">
API Gateway - тестирование производительности функций,
обрабатывающих HTTP-запросы через API Gateway. Измеряется время
отклика, пропускная способность и стабильность работы.
</Typography>
</CardContent>
</Card>
</Grid> </Grid>
</Grid> </Grid>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end' }}>
<Button <Button
variant="contained" variant="contained"
startIcon={<PlayArrowIcon />} color="primary"
onClick={handleStartTest} onClick={handleStartTest}
disabled={testRunning} disabled={!isFormValid}
> >
Запустить тестирование Запустить тестирование
</Button> </Button>
...@@ -226,205 +379,115 @@ function TestingPage() { ...@@ -226,205 +379,115 @@ function TestingPage() {
{activeStep === 1 && ( {activeStep === 1 && (
<Box> <Box>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Выполнение тестов Выполнение тестирования
</Typography> </Typography>
<LinearProgress variant="determinate" value={progress} sx={{ height: 10, mb: 2 }} />
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Card sx={{ mb: 4, bgcolor: '#f8f8f8' }}>
Прогресс: {progress}% ({Math.floor(iterations * progress / 100)} из {iterations} итераций) <CardContent>
</Typography> <Typography variant="subtitle1" gutterBottom fontWeight="bold">
Выбранные параметры:
<Paper variant="outlined" sx={{ p: 2, mb: 3, bgcolor: '#f8f8f8' }}> </Typography>
<Typography variant="subtitle2" gutterBottom> <Grid container spacing={2}>
Лог выполнения: <Grid item xs={12} sm={6}>
</Typography>
<Box sx={{ maxHeight: '250px', overflow: 'auto', fontFamily: 'monospace', fontSize: '0.85rem' }}>
<Box sx={{ py: 0.5 }}>
<Typography variant="body2" component="span" color="text.secondary">[16:42:23]</Typography>
<Typography variant="body2" component="span"> Инициализация тестового окружения...</Typography>
</Box>
<Box sx={{ py: 0.5 }}>
<Typography variant="body2" component="span" color="text.secondary">[16:42:25]</Typography>
<Typography variant="body2" component="span"> Развёртывание тестовой функции в облаке Яндекс...</Typography>
</Box>
<Box sx={{ py: 0.5 }}>
<Typography variant="body2" component="span" color="text.secondary">[16:42:30]</Typography>
<Typography variant="body2" component="span"> Начало тестирования API Gateway...</Typography>
</Box>
<Box sx={{ py: 0.5 }}>
<Typography variant="body2" component="span" color="text.secondary">[16:42:35]</Typography>
<Typography variant="body2" component="span"> Выполнено 25 из {iterations} итераций...</Typography>
</Box>
<Box sx={{ py: 0.5 }}>
<Typography variant="body2" component="span" color="text.secondary">[16:42:40]</Typography>
<Typography variant="body2" component="span"> Сбор первичных метрик...</Typography>
</Box>
</Box>
</Paper>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}>
<Button variant="outlined" onClick={handleReset} disabled={progress < 100 && progress > 0}>
Отменить
</Button>
<Button variant="contained" disabled={progress < 100}>
Анализировать результаты
</Button>
</Box>
</Box>
)}
{activeStep === 2 && (
<Box>
<Typography variant="h6" gutterBottom>
Предварительные результаты
</Typography>
<Typography variant="body2" paragraph>
Результаты тестирования доступны. Система автоматически проанализировала данные
и подготовила рекомендации по оптимизации.
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Card sx={{ mb: 2 }}>
<CardContent>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }}>
<SettingsIcon color="primary" />
<Typography variant="subtitle1">
Метрики производительности
</Typography>
</Stack>
<Typography variant="body2">
<strong>Время ответа (P95):</strong> 124 мс
</Typography>
<Typography variant="body2">
<strong>Время холодного старта:</strong> 320 мс
</Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>Пропускная способность:</strong> 82 TPS <strong>Функция:</strong> {getSelectedFunctionName()}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>Использование ЦП:</strong> 32% <strong>Рантайм:</strong> {runtimeOptions.find(r => r.value === selectedRuntime)?.label || ''}
</Typography> </Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="body2"> <Typography variant="body2">
<strong>Использование RAM:</strong> 256 МБ <strong>Итераций:</strong> {iterations}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6}>
<Card>
<CardContent>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }}>
<BugReportIcon color="primary" />
<Typography variant="subtitle1">
Выявленные проблемы
</Typography>
</Stack>
<Typography variant="body2" sx={{ mb: 1 }}>
Высокое время холодного старта
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Избыточное потребление памяти
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Неоптимальные HTTP-заголовки кэширования
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
Большой размер пакета развертывания <strong>Параллельных запусков:</strong> {parallelRuns}
</Typography> </Typography>
</CardContent> </Grid>
</Card> </Grid>
</Grid> </CardContent>
</Grid> </Card>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Button variant="outlined" onClick={handleReset}> <AccessTimeIcon color="primary" sx={{ mr: 1 }} />
Новый тест <Typography variant="h6" component="div">
</Button> {formatTime(elapsedTime)}
</Typography>
</Box>
<Typography variant="body2" gutterBottom>
Прогресс тестирования:
</Typography>
<LinearProgress
variant="determinate"
value={progress}
sx={{ height: 10, borderRadius: 5, mb: 1 }}
/>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
{Math.floor(iterations * progress / 100)} из {iterations} итераций ({progress}%)
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Button <Button
variant="contained" variant="outlined"
onClick={() => setActiveStep(3)} color="error"
onClick={handleCancelTest}
> >
Просмотреть рекомендации Отмена
</Button> </Button>
</Box> </Box>
</Box> </Box>
)} )}
{activeStep === 3 && ( {activeStep === 2 && (
<Box> <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 4 }}>
<Typography variant="h6" gutterBottom> <CheckCircleOutlineIcon color="success" sx={{ fontSize: 80, mb: 2 }} />
Рекомендации по оптимизации
<Typography variant="h6" gutterBottom align="center">
Тестирование функции "{getSelectedFunctionName()}" завершено за {formatTime(testDuration)}.
</Typography> </Typography>
<Typography variant="body2" paragraph>
На основе результатов тестирования система сформировала следующие рекомендации <Typography variant="body1" align="center" sx={{ mb: 4 }}>
для оптимизации производительности функций. Результаты тестирования сохранены.
</Typography> </Typography>
<Card sx={{ mb: 3 }}> <Stack direction="row" spacing={2}>
<CardContent> <Button
<Typography variant="subtitle1" gutterBottom> variant="outlined"
1. Оптимизация холодного старта onClick={handleNewTest}
</Typography> >
<Typography variant="body2" paragraph>
Время холодного старта составляет 320 мс, что превышает оптимальное значение.
Рекомендуется:
</Typography>
<Typography variant="body2" component="ul">
<li>Использовать функциональность предварительного разогрева</li>
<li>Уменьшить размер пакета развертывания с 45 МБ до 20 МБ</li>
<li>Оптимизировать импорты, исключив неиспользуемые зависимости</li>
<li>Внедрить механизм кэширования соединений с внешними сервисами</li>
</Typography>
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" gutterBottom>
2. Оптимизация использования памяти
</Typography>
<Typography variant="body2" paragraph>
Использование оперативной памяти составляет 256 МБ при пиковой нагрузке.
Рекомендуется:
</Typography>
<Typography variant="body2" component="ul">
<li>Оптимизировать алгоритмы обработки данных для снижения потребления памяти</li>
<li>Внедрить потоковую обработку вместо загрузки всего набора данных в память</li>
<li>Настроить конфигурацию выделения памяти в соответствии с требованиями функции</li>
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography variant="subtitle1" gutterBottom>
3. Оптимизация HTTP-взаимодействия
</Typography>
<Typography variant="body2" paragraph>
Время ответа составляет 124 мс (P95). Рекомендуется:
</Typography>
<Typography variant="body2" component="ul">
<li>Настроить правильные заголовки кэширования для статичных ответов</li>
<li>Использовать оптимизацию сжатия gzip/brotli для уменьшения объема передаваемых данных</li>
<li>Разделить функцию на несколько более специализированных для улучшения масштабирования</li>
</Typography>
</CardContent>
</Card>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}>
<Button variant="outlined" onClick={handleReset}>
Новый тест Новый тест
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
onClick={() => console.log('Применение оптимизаций')} onClick={handleViewResults}
> >
Применить оптимизации Просмотр результатов
</Button> </Button>
</Box> </Stack>
</Box> </Box>
)} )}
</Paper> </Paper>
{/* Диалог подтверждения отмены */}
<Dialog
open={isDialogOpen}
onClose={handleDialogClose}
>
<DialogTitle>Подтверждение отмены</DialogTitle>
<DialogContent>
<DialogContentText>
Вы уверены, что хотите отменить текущее тестирование?
Все промежуточные результаты будут потеряны.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogClose}>Нет</Button>
<Button onClick={handleConfirmCancel} color="error" autoFocus>
Да, отменить
</Button>
</DialogActions>
</Dialog>
</Box> </Box>
</Container> </Container>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment