Compare commits
20
Commits
v1.0.0
...
6f061e16bc
No files matched your search
+10
-2
@@ -1,5 +1,13 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist/
|
dist
|
||||||
|
dist.tar.gz
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitea
|
||||||
|
.gitattributes
|
||||||
|
.claude
|
||||||
|
.wolf
|
||||||
|
.superset
|
||||||
|
docs
|
||||||
*.md
|
*.md
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# Documentação e configuração de ferramenta não mudam nada que a suíte teste.
|
||||||
|
# Na main não roda por push: o release.yml é quem a chama, e só quando a imagem
|
||||||
|
# precisa ser reconstruída; rodar nos dois repetiria a mesma suíte no mesmo commit.
|
||||||
|
# Sem gatilho de pull_request: o push da branch já valida o mesmo commit, e os
|
||||||
|
# dois juntos rodariam tudo duas vezes (o repositório não recebe PR de fork).
|
||||||
|
#
|
||||||
|
# A imagem é construída aqui uma única vez, em arm64 (a arquitetura do servidor),
|
||||||
|
# testada e publicada como inputs-<hash>. Ao fazer o merge, o release encontra
|
||||||
|
# essa tag e só aplica os nomes novos, sem rodar nem construir nada de novo.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore: [main]
|
||||||
|
paths-ignore: ['**.md', 'LICENSE', 'CITATION.cff', 'paper.md', 'references.bib', 'docs/**', '.claude/**', '.wolf/**', '.superset/**', 'scripts/release-notes.sh']
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * 1' # toda segunda: reavalia com a base de vulnerabilidades nova
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verificar:
|
||||||
|
name: Tipos e testes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
|
||||||
|
# tsc -b é o mesmo que o build faz antes do Vite; aqui falha antes de gastar a imagem.
|
||||||
|
- run: npx tsc -b
|
||||||
|
- run: npm test
|
||||||
|
|
||||||
|
# Trivy roda pela imagem oficial: o trivy-action clona o repositório do Trivy
|
||||||
|
# do mesmo servidor do workflow, que aqui é o Gitea, e não o GitHub.
|
||||||
|
# O workspace do job é um volume que o daemon não enxerga, então os arquivos
|
||||||
|
# entram no container com `docker cp` em vez de `-v`.
|
||||||
|
seguranca:
|
||||||
|
name: Trivy (dependências e segredos)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Repositório
|
||||||
|
run: |
|
||||||
|
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
|
||||||
|
fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 .)
|
||||||
|
docker cp . "$cid:/src"
|
||||||
|
docker start -a "$cid"
|
||||||
|
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
exit "$status"
|
||||||
|
|
||||||
|
# Só começa depois das verificações baratas: um teste quebrado não gasta um
|
||||||
|
# build de imagem. Roda no runner arm64, o mesmo tipo do servidor, então o
|
||||||
|
# que é escaneado e testado é a imagem que vai ao ar.
|
||||||
|
imagem:
|
||||||
|
name: Imagem Docker (linux/arm64)
|
||||||
|
needs: [verificar, seguranca]
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
env:
|
||||||
|
REGISTRY: git.lucasrguerra.dev.br
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Conteúdo já publicado (re-push, tag num commit já validado): nada a fazer.
|
||||||
|
- id: plano
|
||||||
|
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# `docker build` simples, no daemon persistente do runner: as camadas e o
|
||||||
|
# cache mount do npm sobrevivem entre execuções. Um buildx criaria um
|
||||||
|
# BuildKit novo, e vazio, a cada job.
|
||||||
|
- name: Build
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: docker build -t pss:ci .
|
||||||
|
|
||||||
|
- name: Trivy na imagem
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: |
|
||||||
|
docker run --rm --network host -v trivy-cache:/root/.cache/ -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \
|
||||||
|
image --severity HIGH,CRITICAL --exit-code 1 pss:ci
|
||||||
|
|
||||||
|
# A SPA sobe, cai no index nas rotas internas e entrega os assets com
|
||||||
|
# cache imutável (scripts/smoke-imagem.sh).
|
||||||
|
- name: A imagem sobe e responde
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: |
|
||||||
|
docker run -d --name pss-ci -p 127.0.0.1:3998:8080 pss:ci
|
||||||
|
status=0
|
||||||
|
scripts/smoke-imagem.sh http://127.0.0.1:3998 || status=1
|
||||||
|
docker rm -f pss-ci >/dev/null
|
||||||
|
exit "$status"
|
||||||
|
|
||||||
|
# O login fica por último: build, Trivy e smoke não precisam de credencial.
|
||||||
|
- if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
# Passou em tudo: publica a mesma imagem que foi testada, sem reconstruir.
|
||||||
|
- name: Publica inputs-<hash>
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: |
|
||||||
|
tag="${{ steps.plano.outputs.image }}:inputs-${{ steps.plano.outputs.inputs }}"
|
||||||
|
docker tag pss:ci "$tag"
|
||||||
|
docker push "$tag"
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Publica a imagem no registro de containers do Gitea depois de um merge em main,
|
||||||
|
# e de novo com tag semver quando uma tag v* é enviada. O servidor só faz pull.
|
||||||
|
# Precisa do secret REGISTRY_TOKEN no repositório (token do Gitea com write:package):
|
||||||
|
# o registro recusa o token do job.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.lucasrguerra.dev.br
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# A imagem só depende dos arquivos que entram no build, não da documentação nem
|
||||||
|
# da configuração de ferramentas. O push da branch já a construiu, testou e
|
||||||
|
# publicou como inputs-<hash>; quando essa tag existe (o caso normal de um
|
||||||
|
# merge, uma tag num commit validado, um merge só de docs), não há o que
|
||||||
|
# construir: as tags novas apontam para o manifesto existente.
|
||||||
|
plano:
|
||||||
|
name: Plano (reaproveitar a imagem?)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
inputs: ${{ steps.plano.outputs.inputs }}
|
||||||
|
reuse: ${{ steps.plano.outputs.reuse }}
|
||||||
|
image: ${{ steps.plano.outputs.image }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- id: plano
|
||||||
|
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Nada é publicado sem a suíte completa passar antes. Pulada quando a imagem é
|
||||||
|
# reaproveitada: o mesmo conteúdo já passou por ela.
|
||||||
|
ci:
|
||||||
|
needs: plano
|
||||||
|
if: ${{ needs.plano.outputs.reuse != 'true' }}
|
||||||
|
uses: ./.gitea/workflows/ci.yml
|
||||||
|
# Workflows reutilizáveis não herdam os secrets sozinhos: o login no registro precisa deles.
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
publicar:
|
||||||
|
name: Publica a imagem (linux/arm64)
|
||||||
|
needs: [plano, ci]
|
||||||
|
# ci é pulado, e não bem-sucedido, quando a imagem é reaproveitada.
|
||||||
|
if: ${{ !cancelled() && needs.plano.result == 'success' && (needs.ci.result == 'success' || needs.plano.outputs.reuse == 'true') }}
|
||||||
|
# O único runner é arm64 e o servidor de destino (Oracle) também, então a
|
||||||
|
# imagem é construída nativamente, sem emulação.
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Se o ci chamado acima acabou de publicar a imagem (push direto na main),
|
||||||
|
# o plano lá de cima está desatualizado: confere de novo antes de construir.
|
||||||
|
- id: replano
|
||||||
|
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Autentica no registro
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Metadados
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: ${{ needs.plano.outputs.image }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest
|
||||||
|
type=sha,format=long
|
||||||
|
|
||||||
|
- name: Build e push
|
||||||
|
if: ${{ steps.replano.outputs.reuse != 'true' }}
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=${{ needs.plano.outputs.image }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ needs.plano.outputs.image }}:buildcache,mode=max
|
||||||
|
|
||||||
|
# Mesmo manifesto, nomes novos: nenhuma camada é lida nem escrita.
|
||||||
|
- name: Aplica as tags à imagem existente
|
||||||
|
if: ${{ steps.replano.outputs.reuse == 'true' }}
|
||||||
|
run: |
|
||||||
|
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
||||||
|
docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}"
|
||||||
|
|
||||||
|
# Cria a entrada em "Releases" quando a execução é de uma tag v*. Roda depois
|
||||||
|
# de publicar a imagem, com as notas da seção do CHANGELOG.md.
|
||||||
|
release:
|
||||||
|
name: Cria a release no Gitea
|
||||||
|
needs: [plano, publicar]
|
||||||
|
# Explícito, como o publicar: o ci acima é pulado quando a imagem é
|
||||||
|
# reaproveitada, e o success() implícito pularia este job também.
|
||||||
|
if: ${{ !cancelled() && needs.publicar.result == 'success' && startsWith(gitea.ref, 'refs/tags/v') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
IMAGE: ${{ needs.plano.outputs.image }}
|
||||||
|
run: |
|
||||||
|
scripts/release-notes.sh "$TAG" "$IMAGE" > notes.md
|
||||||
|
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
||||||
|
jq -n --arg tag "$TAG" --arg name "PSS ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
||||||
|
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||||
|
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' -d @release.json
|
||||||
+7
-16
@@ -1,19 +1,10 @@
|
|||||||
!**/glob-import/dir/node_modules
|
node_modules
|
||||||
.DS_Store
|
|
||||||
.idea
|
|
||||||
.pnpm-store
|
|
||||||
*.cpuprofile
|
|
||||||
*.local
|
|
||||||
*.log
|
|
||||||
/.vscode/
|
|
||||||
/docs/.vitepress/cache
|
|
||||||
/docs/.vitepress/.temp
|
|
||||||
/packages/vite/LICENSE
|
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
explorations
|
dist.tar.gz
|
||||||
node_modules
|
*.local
|
||||||
playground-temp
|
*.log
|
||||||
temp
|
.DS_Store
|
||||||
TODOs.md
|
.idea
|
||||||
|
/.vscode/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Everything notable in PSS is recorded here. The format follows
|
||||||
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows
|
||||||
|
[Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.3.3] - 2026-11-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The code, the container image and the releases now live on Gitea
|
||||||
|
(`git.lucasrguerra.dev.br`). The deployment pulls the image from its registry.
|
||||||
|
- The `Dockerfile` is multi-stage: it builds Vite from source inside the image instead
|
||||||
|
of unpacking a `dist.tar.gz` committed to the repository (removed, along with the
|
||||||
|
`build:release` script). The final stage is `nginx-unprivileged`: the container runs
|
||||||
|
as the `nginx` user and listens on port 8080 instead of 80 (Trivy DS-0002).
|
||||||
|
- New CI on Gitea runners (`.gitea/workflows`): types, 282 tests, Trivy on the repository
|
||||||
|
and on the image, and an image smoke test. The image is built once per content hash and
|
||||||
|
reused by the release, so a merge or a tag on an already validated commit rebuilds nothing.
|
||||||
|
- Repository links in `package.json`, `CITATION.cff`, the READMEs and `CONTRIBUTING.md`
|
||||||
|
point to Gitea. `.gitattributes` forces LF line endings.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `docker-compose.yml` healthcheck uses `CMD-SHELL` and the correct local URL, and the
|
||||||
|
port mapping honours `PORT`.
|
||||||
|
|
||||||
|
## [1.3.2] - 2026-03-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Regression tests for Round Robin scheduling with threads.
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-03-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ThreadMetricsChart` and per-thread metrics.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improvements to the Gantt chart and the metrics panel.
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-03-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Multilevel Feedback Queue (MLFQ) scheduling algorithm.
|
||||||
|
- Thread scheduler.
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-03-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- English documentation, a `CONTRIBUTING.md`, the JOSS paper and a Zenodo DOI in
|
||||||
|
`CITATION.cff`.
|
||||||
|
- Multilevel Queue (MLQ) scheduling algorithm and its documentation.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- License switched to MIT.
|
||||||
|
- Scheduling algorithms refactored into a class-based `BaseAlgorithm` structure, with
|
||||||
|
the engine and tests updated and new algorithm and metrics documentation.
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-03-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- First release of the browser-based CPU scheduling simulator: core scheduling engine,
|
||||||
|
algorithms (including Priority Round Robin), Gantt chart with context-switch handling,
|
||||||
|
UI components and Docker setup.
|
||||||
|
|
||||||
|
[Unreleased]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.3.3...HEAD
|
||||||
|
[1.3.3]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.3.2...v1.3.3
|
||||||
|
[1.3.2]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.2.0...v1.3.2
|
||||||
|
[1.2.0]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.0.0...v1.2.0
|
||||||
|
[1.0.0]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/releases/tag/v1.0.0
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
message: "If you use this software, please cite it as below."
|
||||||
|
type: software
|
||||||
|
title: "PSS: A Browser-Based Interactive Simulator for CPU Scheduling Algorithms"
|
||||||
|
version: 1.2.0
|
||||||
|
date-released: "2026-03-23"
|
||||||
|
license: MIT
|
||||||
|
url: "https://pss.lucasrguerra.dev.br"
|
||||||
|
repository-code: "https://git.lucasrguerra.dev.br/lucasrguerra/pss"
|
||||||
|
doi: "10.5281/zenodo.19197235"
|
||||||
|
identifiers:
|
||||||
|
- type: doi
|
||||||
|
value: "10.5281/zenodo.19197235"
|
||||||
|
description: "Zenodo archive of PSS v1.2.0"
|
||||||
|
authors:
|
||||||
|
- family-names: Guerra
|
||||||
|
given-names: Lucas Rayan
|
||||||
|
orcid: "https://orcid.org/0009-0002-3170-4126"
|
||||||
|
affiliation: "Universidade Federal Rural de Pernambuco (UFRPE), Brazil"
|
||||||
|
email: lucas.guerra@ufrpe.br
|
||||||
|
keywords:
|
||||||
|
- operating systems
|
||||||
|
- CPU scheduling
|
||||||
|
- process scheduling
|
||||||
|
- computer science education
|
||||||
|
- simulation
|
||||||
|
- visualization
|
||||||
|
- TypeScript
|
||||||
|
- React
|
||||||
+4
-4
@@ -10,7 +10,7 @@ This document provides guidelines and instructions for contributing to this proj
|
|||||||
|
|
||||||
This section guides you through submitting a bug report for PSS. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
This section guides you through submitting a bug report for PSS. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||||
|
|
||||||
If you find a bug, please create an issue on GitHub and include:
|
If you find a bug, please create an issue on Gitea and include:
|
||||||
- A clear and descriptive title.
|
- A clear and descriptive title.
|
||||||
- Exact steps to reproduce the problem.
|
- Exact steps to reproduce the problem.
|
||||||
- What you expected to happen, and what actually happened.
|
- What you expected to happen, and what actually happened.
|
||||||
@@ -37,10 +37,10 @@ To contribute code, you'll need to set up the project locally.
|
|||||||
|
|
||||||
#### Installation Steps
|
#### Installation Steps
|
||||||
|
|
||||||
1. **Fork the repository** on GitHub.
|
1. **Fork the repository** on Gitea.
|
||||||
2. **Clone your fork** locally:
|
2. **Clone your fork** locally:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YOUR_USERNAME/pss.git
|
git clone https://git.lucasrguerra.dev.br/YOUR_USERNAME/pss.git
|
||||||
cd pss
|
cd pss
|
||||||
```
|
```
|
||||||
3. **Install dependencies**:
|
3. **Install dependencies**:
|
||||||
@@ -78,7 +78,7 @@ If you add a new feature, please include appropriate tests covering the new func
|
|||||||
```bash
|
```bash
|
||||||
git commit -m "feat: add awesome new feature"
|
git commit -m "feat: add awesome new feature"
|
||||||
```
|
```
|
||||||
4. Push your branch to your fork on GitHub:
|
4. Push your branch to your fork on Gitea:
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/my-awesome-feature
|
git push origin feature/my-awesome-feature
|
||||||
```
|
```
|
||||||
|
|||||||
+19
-8
@@ -1,13 +1,24 @@
|
|||||||
# ── Production image ──
|
# syntax=docker/dockerfile:1
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
|
# ── Build: compila o Vite a partir do código, sem artefato pré-construído ──
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN --mount=type=cache,target=/root/.npm npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Imagem final: nginx sem privilégios (usuário nginx, porta 8080) e os estáticos ──
|
||||||
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
# Aplica as correções de segurança do Alpine publicadas depois da imagem base.
|
||||||
|
USER root
|
||||||
|
RUN apk upgrade --no-cache
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# Unpack pre-built Vite output
|
USER nginx
|
||||||
COPY dist.tar.gz /usr/share/nginx/html/
|
EXPOSE 8080
|
||||||
RUN tar -xzf /usr/share/nginx/html/dist.tar.gz --strip-components=1 -C /usr/share/nginx/html \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||||
&& rm /usr/share/nginx/html/dist.tar.gz
|
CMD wget -q --spider http://127.0.0.1:8080/ || exit 1
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
+78
-27
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
## ✨ What is it?
|
## ✨ What is it?
|
||||||
|
|
||||||
The **Process Scheduler Simulator (PSS)** runs entirely in the browser — no server, no system dependencies. You define the processes, choose the algorithm, and watch the simulation unfold in real time through the Gantt chart.
|
The **Process Scheduler Simulator (PSS)** runs entirely in the browser, completely free of servers and system dependencies. You define the processes, choose the algorithm, and watch the simulation unfold in real time through the Gantt chart.
|
||||||
|
|
||||||
Ideal for:
|
Ideal for:
|
||||||
- 📚 Operating Systems students who want to visualize what was once only theory
|
- 📚 Operating Systems students who want to visualize what was once only theory
|
||||||
@@ -35,7 +35,7 @@ A live demo is available at: **https://pss.lucasrguerra.dev.br/**
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/lucasrguerra/pss.git
|
git clone https://git.lucasrguerra.dev.br/lucasrguerra/pss.git
|
||||||
cd pss
|
cd pss
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
@@ -57,17 +57,44 @@ npm run preview
|
|||||||
### With Docker
|
### With Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `Dockerfile` builds Vite inside the image (Node 22) and serves the static files
|
||||||
|
with an unprivileged nginx (`nginx` user, port 8080). There is no pre-built artifact in the repository.
|
||||||
|
|
||||||
|
### Published image
|
||||||
|
|
||||||
|
Every version is published to the Gitea container registry (linux/arm64):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 git.lucasrguerra.dev.br/lucasrguerra/pss:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Tags: `latest`, `X.Y.Z`, `X.Y` and `sha-<commit>`. The public instance runs at
|
||||||
|
[pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br).
|
||||||
|
|
||||||
|
### Continuous integration and releases
|
||||||
|
|
||||||
|
- `.gitea/workflows/ci.yml`: types (`tsc -b`), tests, Trivy (dependencies and secrets),
|
||||||
|
image build, Trivy on the image and a smoke test (the SPA responds, falls back to the
|
||||||
|
index on internal routes and serves assets with immutable caching). Runs on branches
|
||||||
|
and every Monday.
|
||||||
|
- `.gitea/workflows/release.yml`: on `main` and `vX.Y.Z` tags. If the registry already
|
||||||
|
has the image for the same content (`inputs-<hash>`), nothing is rebuilt: only the new
|
||||||
|
tags are applied. On a tag, it creates the Gitea release with the [CHANGELOG](CHANGELOG.md) notes.
|
||||||
|
- To release: update the CHANGELOG, the version in `package.json` and the label in
|
||||||
|
`src/components/Layout/Sidebar.tsx`, commit, and push the tag
|
||||||
|
(`git tag vX.Y.Z && git push gitea vX.Y.Z`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎮 How to use
|
## 🎮 How to use
|
||||||
|
|
||||||
1. **Create your processes** — In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Or use the random generator to get started quickly.
|
1. **Create your processes**: In the left panel, add processes by defining name, arrival time, priority, and a sequence of bursts (CPU and I/O). Enable the **Use Threads** toggle to create threads within a process, choosing the model (MANY_TO_ONE, ONE_TO_ONE, or MANY_TO_MANY) and the intra-process policy (FCFS, RR, or Priority).
|
||||||
2. **Configure the scheduler** — Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, etc.).
|
2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, quantums and boost interval for MLFQ, etc.).
|
||||||
3. **Run the simulation** — Use the Play/Pause/Step controls to run the simulation at the speed you want (0.25× to 4×).
|
3. **Run the simulation**: Use the Play/Pause/Step controls to run the simulation at the speed you want (0.25× to 4×).
|
||||||
4. **Analyze the results** — The Gantt chart shows each process's state in real time. At the end, the metrics panel displays turnaround, waiting time, throughput, and more.
|
4. **Analyze the results**: The Gantt chart shows each process's state in real time (and its threads, when configured). At the end, the metrics panel displays turnaround, waiting time, throughput, and — in the Threads tab — individual per-thread metrics.
|
||||||
|
|
||||||
> 💡 **Tip:** Load one of the built-in **presets** to see classic phenomena such as the FCFS convoy effect or priority starvation.
|
> 💡 **Tip:** Load one of the built-in **presets** to see classic phenomena such as the FCFS convoy effect or priority starvation.
|
||||||
|
|
||||||
@@ -77,14 +104,16 @@ docker compose up
|
|||||||
|
|
||||||
| Algorithm | Preemptive? | Parameters |
|
| Algorithm | Preemptive? | Parameters |
|
||||||
|-----------|:-----------:|------------|
|
|-----------|:-----------:|------------|
|
||||||
| **FCFS** — First Come, First Served | ❌ | — |
|
| **FCFS**: First Come, First Served | ❌ | None |
|
||||||
| **SJF** — Shortest Job First | ❌ | — |
|
| **SJF**: Shortest Job First | ❌ | None |
|
||||||
| **SRTF** — Shortest Remaining Time First | ✅ | — |
|
| **SRTF**: Shortest Remaining Time First | ✅ | None |
|
||||||
| **Round Robin (RR)** | ✅ | Quantum |
|
| **Round Robin (RR)** | ✅ | Quantum |
|
||||||
| **Priority NP** — Non-Preemptive Priority | ❌ | Aging (optional) |
|
| **Priority NP**: Non-Preemptive Priority | ❌ | Aging (optional) |
|
||||||
| **Priority P** — Preemptive Priority | ✅ | Aging (optional) |
|
| **Priority P**: Preemptive Priority | ✅ | Aging (optional) |
|
||||||
| **HRRN** — Highest Response Ratio Next | ❌ | — |
|
| **Priority RR**: Priority with Round Robin | ✅ | Quantum + Aging (optional) |
|
||||||
|
| **HRRN**: Highest Response Ratio Next | ❌ | None |
|
||||||
| **Multilevel Queue** | ✅ | Quantum per queue |
|
| **Multilevel Queue** | ✅ | Quantum per queue |
|
||||||
|
| **MLFQ**: Multilevel Feedback Queue | ✅ | Quantums per queue + boost interval |
|
||||||
|
|
||||||
> See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works.
|
> See the [algorithms documentation](./docs/algorithms.en.md) for details on how each one works.
|
||||||
|
|
||||||
@@ -94,12 +123,12 @@ docker compose up
|
|||||||
|
|
||||||
For each simulated process, PSS automatically computes:
|
For each simulated process, PSS automatically computes:
|
||||||
|
|
||||||
- **Response Time** — time until the first CPU usage
|
- **Response Time**: time until the first CPU usage
|
||||||
- **Turnaround Time** — total time from arrival to completion
|
- **Turnaround Time**: total time from arrival to completion
|
||||||
- **Waiting Time** — total time spent in the Ready queue
|
- **Waiting Time**: total time spent in the Ready queue
|
||||||
- **CPU Time / I/O Time** — effective time in each type of burst
|
- **CPU Time / I/O Time**: effective time in each type of burst
|
||||||
- **CPU Utilization** — percentage of turnaround time spent on CPU
|
- **CPU Utilization**: percentage of turnaround time spent on CPU
|
||||||
- **Bound Type** — automatic classification as CPU Bound, I/O Bound, or Balanced
|
- **Bound Type**: automatic classification as CPU Bound, I/O Bound, or Balanced
|
||||||
|
|
||||||
Globally: averages for response/turnaround/waiting, throughput, and overall CPU utilization.
|
Globally: averages for response/turnaround/waiting, throughput, and overall CPU utilization.
|
||||||
|
|
||||||
@@ -109,7 +138,7 @@ Globally: averages for response/turnaround/waiting, throughput, and overall CPU
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── core/ # Pure logic — no React
|
├── core/ # Pure logic (no React)
|
||||||
│ ├── algorithms/ # Implementation of each algorithm
|
│ ├── algorithms/ # Implementation of each algorithm
|
||||||
│ │ ├── base.ts # Abstract class BaseScheduler
|
│ │ ├── base.ts # Abstract class BaseScheduler
|
||||||
│ │ ├── fcfs.ts
|
│ │ ├── fcfs.ts
|
||||||
@@ -119,20 +148,30 @@ src/
|
|||||||
│ │ ├── priority_rr.ts
|
│ │ ├── priority_rr.ts
|
||||||
│ │ ├── hrrn.ts
|
│ │ ├── hrrn.ts
|
||||||
│ │ ├── multilevel.ts
|
│ │ ├── multilevel.ts
|
||||||
|
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||||
│ │ └── index.ts # Dispatch by algorithm
|
│ │ └── index.ts # Dispatch by algorithm
|
||||||
│ ├── engine.ts # SimulationEngine — orchestrates the ticks
|
│ ├── engine.ts # SimulationEngine, orchestrates the ticks (thread-aware)
|
||||||
│ ├── metrics.ts # Per-process and global metrics computation
|
│ ├── thread-scheduler.ts # Intra-process scheduler (FCFS / RR / Priority)
|
||||||
|
│ ├── metrics.ts # Per-process, global, and per-thread metrics computation
|
||||||
│ ├── types.ts # All TypeScript interfaces
|
│ ├── types.ts # All TypeScript interfaces
|
||||||
│ └── presets.ts # Pre-defined scenarios
|
│ └── presets.ts # Pre-defined scenarios (processes and threads)
|
||||||
│
|
│
|
||||||
├── store/ # Global state with Zustand
|
├── store/ # Global state with Zustand
|
||||||
├── components/ # React components
|
├── components/ # React components
|
||||||
│ ├── ProcessPanel/ # Process creation and editing
|
│ ├── ProcessPanel/ # Process creation and editing
|
||||||
|
│ │ ├── ThreadEditor.tsx # Thread editor (model, policy, per-thread bursts)
|
||||||
|
│ │ └── ...
|
||||||
│ ├── SchedulerPanel/ # Algorithm selection and parameters
|
│ ├── SchedulerPanel/ # Algorithm selection and parameters
|
||||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
|
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
|
||||||
│ ├── GanttChart/ # Interactive Gantt chart
|
│ ├── GanttChart/ # Interactive Gantt chart
|
||||||
|
│ │ ├── GanttThreadRow.tsx # Thread sub-row in the Gantt chart
|
||||||
|
│ │ └── ...
|
||||||
│ ├── MetricsPanel/ # Table, charts, and global metrics
|
│ ├── MetricsPanel/ # Table, charts, and global metrics
|
||||||
|
│ │ ├── ThreadMetricsTable.tsx # Per-thread metrics table
|
||||||
|
│ │ └── ...
|
||||||
│ └── shared/ # Buttons, badges, tooltips, modals
|
│ └── shared/ # Buttons, badges, tooltips, modals
|
||||||
|
│ ├── threadUtils.ts # Thread color derivation (HSL)
|
||||||
|
│ └── ...
|
||||||
│
|
│
|
||||||
└── hooks/ # Custom hooks (animation loop, export, etc.)
|
└── hooks/ # Custom hooks (animation loop, export, etc.)
|
||||||
```
|
```
|
||||||
@@ -143,6 +182,8 @@ src/
|
|||||||
|
|
||||||
Load classic scenarios with a single click to explore OS phenomena:
|
Load classic scenarios with a single click to explore OS phenomena:
|
||||||
|
|
||||||
|
**Processes:**
|
||||||
|
|
||||||
| Preset | What it demonstrates |
|
| Preset | What it demonstrates |
|
||||||
|--------|----------------------|
|
|--------|----------------------|
|
||||||
| `classic_fcfs` | Basic execution with FCFS, no I/O |
|
| `classic_fcfs` | Basic execution with FCFS, no I/O |
|
||||||
@@ -153,6 +194,16 @@ Load classic scenarios with a single click to explore OS phenomena:
|
|||||||
| `io_heavy` | Mix of CPU Bound vs. I/O Bound processes |
|
| `io_heavy` | Mix of CPU Bound vs. I/O Bound processes |
|
||||||
| `multilevel_demo` | Multiple queues with different priorities |
|
| `multilevel_demo` | Multiple queues with different priorities |
|
||||||
|
|
||||||
|
**Threads:**
|
||||||
|
|
||||||
|
| Preset | What it demonstrates |
|
||||||
|
|--------|----------------------|
|
||||||
|
| `many_to_one` | N:1 model — any thread's I/O blocks the entire process |
|
||||||
|
| `one_to_one` | 1:1 model — threads block independently |
|
||||||
|
| `many_to_many` | N:M model — kernel slots limit parallelism |
|
||||||
|
| `thread_starvation` | Intra-process starvation with Priority policy |
|
||||||
|
| `threads_vs_processes` | Direct comparison: threaded process vs single-burst process |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Tests
|
## 🧪 Tests
|
||||||
@@ -189,9 +240,9 @@ Tests cover the simulation engine, all scheduling algorithms, and metrics comput
|
|||||||
|
|
||||||
## 📄 Additional Documentation
|
## 📄 Additional Documentation
|
||||||
|
|
||||||
- [📐 Scheduling Algorithms](./docs/algorithms.en.md) — detailed description of each algorithm, tie-breaking rules, and examples
|
- [📐 Scheduling Algorithms](./docs/algorithms.en.md): detailed description of each algorithm, tie-breaking rules, and examples
|
||||||
- [📏 Metrics](./docs/metrics.en.md) — formulas and definitions for all computed metrics
|
- [📏 Metrics](./docs/metrics.en.md): formulas and definitions for all computed metrics
|
||||||
- [📝 JOSS Paper](./paper.md) — scientific paper submitted to the Journal of Open Source Software
|
- [📝 JOSS Paper](./paper.md): scientific paper submitted to the Journal of Open Source Software
|
||||||
|
|
||||||
*Também disponível em: [🇧🇷 Português](./README.md)*
|
*Também disponível em: [🇧🇷 Português](./README.md)*
|
||||||
|
|
||||||
@@ -205,4 +256,4 @@ Contributions are welcome! Please read our [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|||||||
|
|
||||||
## 📜 License
|
## 📜 License
|
||||||
|
|
||||||
Distributed under the [MIT License](./LICENSE). Developed by [Lucas Rayan Guerra](https://github.com/lucasrguerra).
|
Distributed under the [MIT License](./LICENSE). Developed by [Lucas Rayan Guerra](https://git.lucasrguerra.dev.br/lucasrguerra).
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
## ✨ O que é?
|
## ✨ O que é?
|
||||||
|
|
||||||
O **Process Scheduler Simulator (PSS)** roda completamente no navegador — sem servidor, sem instalação de dependências de sistema. Você define os processos, escolhe o algoritmo, e assiste à simulação acontecer em tempo real através do diagrama de Gantt.
|
O **Process Scheduler Simulator (PSS)** roda completamente no navegador, sem servidor e sem instalação de dependências de sistema. Você define os processos, escolhe o algoritmo, e assiste à simulação acontecer em tempo real através do diagrama de Gantt.
|
||||||
|
|
||||||
Ideal para:
|
Ideal para:
|
||||||
- 📚 Estudantes de Sistemas Operacionais querendo visualizar o que antes era só teoria
|
- 📚 Estudantes de Sistemas Operacionais querendo visualizar o que antes era só teoria
|
||||||
@@ -33,7 +33,7 @@ Ideal para:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone o repositório
|
# Clone o repositório
|
||||||
git clone https://github.com/lucasrguerra/pss.git
|
git clone https://git.lucasrguerra.dev.br/lucasrguerra/pss.git
|
||||||
cd pss
|
cd pss
|
||||||
|
|
||||||
# Instale as dependências
|
# Instale as dependências
|
||||||
@@ -55,17 +55,44 @@ npm run preview
|
|||||||
### Com Docker
|
### Com Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
O `Dockerfile` compila o Vite dentro da própria imagem (Node 22) e entrega os
|
||||||
|
arquivos estáticos por um nginx sem privilégios (usuário `nginx`, porta 8080). Não há artefato pré-construído no repositório.
|
||||||
|
|
||||||
|
### Imagem publicada
|
||||||
|
|
||||||
|
Cada versão é publicada no registro de contêineres do Gitea (linux/arm64):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 git.lucasrguerra.dev.br/lucasrguerra/pss:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Tags: `latest`, `X.Y.Z`, `X.Y` e `sha-<commit>`. A instância pública roda em
|
||||||
|
[pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br).
|
||||||
|
|
||||||
|
### Integração contínua e releases
|
||||||
|
|
||||||
|
- `.gitea/workflows/ci.yml`: tipos (`tsc -b`), testes, Trivy (dependências e segredos),
|
||||||
|
build da imagem, Trivy na imagem e um teste de fumaça (a SPA responde, cai no index
|
||||||
|
nas rotas internas e entrega os assets com cache imutável). Roda nas branches e toda
|
||||||
|
segunda-feira.
|
||||||
|
- `.gitea/workflows/release.yml`: na `main` e nas tags `vX.Y.Z`. Se o registro já tem a
|
||||||
|
imagem do mesmo conteúdo (`inputs-<hash>`), nada é refeito: só as tags novas são
|
||||||
|
aplicadas. Numa tag, cria a release no Gitea com as notas do [CHANGELOG](CHANGELOG.md).
|
||||||
|
- Para lançar: atualize o CHANGELOG, a versão em `package.json` e o rótulo em
|
||||||
|
`src/components/Layout/Sidebar.tsx`, faça o commit e envie a tag
|
||||||
|
(`git tag vX.Y.Z && git push gitea vX.Y.Z`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎮 Como usar
|
## 🎮 Como usar
|
||||||
|
|
||||||
1. **Crie seus processos** — No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ou use o gerador aleatório para começar rápido.
|
1. **Crie seus processos**: No painel esquerdo, adicione processos definindo nome, tempo de chegada, prioridade e sequência de bursts (CPU e I/O). Ative a opção **Usar Threads** para criar threads dentro do processo, escolhendo o modelo (MANY_TO_ONE, ONE_TO_ONE ou MANY_TO_MANY) e a política intra-processo (FCFS, RR ou Priority).
|
||||||
2. **Configure o escalonador** — Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.).
|
2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, quantuns e boost para MLFQ, etc.).
|
||||||
3. **Execute a simulação** — Use os controles de Play/Pause/Step para rodar a simulação na velocidade que quiser (0.25× até 4×).
|
3. **Execute a simulação**: Use os controles de Play/Pause/Step para rodar a simulação na velocidade que quiser (0.25× até 4×).
|
||||||
4. **Analise os resultados** — O diagrama de Gantt mostra em tempo real o estado de cada processo. Ao final, o painel de métricas exibe turnaround, waiting time, throughput e mais.
|
4. **Analise os resultados**: O diagrama de Gantt mostra em tempo real o estado de cada processo (e suas threads, quando configuradas). Ao final, o painel de métricas exibe turnaround, waiting time, throughput, e — na aba Threads — métricas individuais por thread.
|
||||||
|
|
||||||
> 💡 **Dica:** Carregue um dos **presets** disponíveis para ver fenômenos clássicos como o efeito de comboio do FCFS ou starvation com Priority Scheduling.
|
> 💡 **Dica:** Carregue um dos **presets** disponíveis para ver fenômenos clássicos como o efeito de comboio do FCFS ou starvation com Priority Scheduling.
|
||||||
|
|
||||||
@@ -75,14 +102,16 @@ docker compose up
|
|||||||
|
|
||||||
| Algoritmo | Preemptivo? | Parâmetros |
|
| Algoritmo | Preemptivo? | Parâmetros |
|
||||||
|-----------|:-----------:|------------|
|
|-----------|:-----------:|------------|
|
||||||
| **FCFS** — First Come, First Served | ❌ | — |
|
| **FCFS**: First Come, First Served | ❌ | Nenhum |
|
||||||
| **SJF** — Shortest Job First | ❌ | — |
|
| **SJF**: Shortest Job First | ❌ | Nenhum |
|
||||||
| **SRTF** — Shortest Remaining Time First | ✅ | — |
|
| **SRTF**: Shortest Remaining Time First | ✅ | Nenhum |
|
||||||
| **Round Robin (RR)** | ✅ | Quantum |
|
| **Round Robin (RR)** | ✅ | Quantum |
|
||||||
| **Priority NP** — Prioridade Não-Preemptivo | ❌ | Aging (opcional) |
|
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
|
||||||
| **Priority P** — Prioridade Preemptivo | ✅ | Aging (opcional) |
|
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
|
||||||
| **HRRN** — Highest Response Ratio Next | ❌ | — |
|
| **Priority RR**: Prioridade com Round Robin | ✅ | Quantum + Aging (opcional) |
|
||||||
|
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
|
||||||
| **Multilevel Queue** | ✅ | Quantum por fila |
|
| **Multilevel Queue** | ✅ | Quantum por fila |
|
||||||
|
| **MLFQ**: Multilevel Feedback Queue | ✅ | Quantuns por fila + intervalo de boost |
|
||||||
|
|
||||||
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
|
> Consulte a [documentação dos algoritmos](./docs/algorithms.md) para detalhes sobre como cada um funciona.
|
||||||
|
|
||||||
@@ -92,12 +121,12 @@ docker compose up
|
|||||||
|
|
||||||
Para cada processo simulado, o PSS calcula automaticamente:
|
Para cada processo simulado, o PSS calcula automaticamente:
|
||||||
|
|
||||||
- **Response Time** — tempo até o primeiro uso de CPU
|
- **Response Time**: tempo até o primeiro uso de CPU
|
||||||
- **Turnaround Time** — tempo total desde a chegada até o término
|
- **Turnaround Time**: tempo total desde a chegada até o término
|
||||||
- **Waiting Time** — tempo total na fila Ready
|
- **Waiting Time**: tempo total na fila Ready
|
||||||
- **CPU Time / I/O Time** — tempo efetivo em cada tipo de burst
|
- **CPU Time / I/O Time**: tempo efetivo em cada tipo de burst
|
||||||
- **CPU Utilization** — percentual do turnaround em CPU
|
- **CPU Utilization**: percentual do turnaround em CPU
|
||||||
- **Bound Type** — classificação automática em CPU Bound, I/O Bound ou Balanced
|
- **Bound Type**: classificação automática em CPU Bound, I/O Bound ou Balanced
|
||||||
|
|
||||||
E globalmente: médias de response/turnaround/waiting, throughput e utilização global de CPU.
|
E globalmente: médias de response/turnaround/waiting, throughput e utilização global de CPU.
|
||||||
|
|
||||||
@@ -107,7 +136,7 @@ E globalmente: médias de response/turnaround/waiting, throughput e utilização
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── core/ # Lógica pura — sem React
|
├── core/ # Lógica pura (sem React)
|
||||||
│ ├── algorithms/ # Implementação de cada algoritmo
|
│ ├── algorithms/ # Implementação de cada algoritmo
|
||||||
│ │ ├── base.ts # Classe abstrata BaseScheduler
|
│ │ ├── base.ts # Classe abstrata BaseScheduler
|
||||||
│ │ ├── fcfs.ts
|
│ │ ├── fcfs.ts
|
||||||
@@ -117,20 +146,30 @@ src/
|
|||||||
│ │ ├── priority_rr.ts
|
│ │ ├── priority_rr.ts
|
||||||
│ │ ├── hrrn.ts
|
│ │ ├── hrrn.ts
|
||||||
│ │ ├── multilevel.ts
|
│ │ ├── multilevel.ts
|
||||||
|
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||||
│ │ └── index.ts # Dispatch por algoritmo
|
│ │ └── index.ts # Dispatch por algoritmo
|
||||||
│ ├── engine.ts # SimulationEngine — orquestra os ticks
|
│ ├── engine.ts # SimulationEngine, orquestra os ticks (suporte a threads)
|
||||||
│ ├── metrics.ts # Cálculo de métricas por processo e globais
|
│ ├── thread-scheduler.ts # Escalonador intra-processo (FCFS / RR / Priority)
|
||||||
|
│ ├── metrics.ts # Cálculo de métricas por processo, globais e por thread
|
||||||
│ ├── types.ts # Todas as interfaces TypeScript
|
│ ├── types.ts # Todas as interfaces TypeScript
|
||||||
│ └── presets.ts # Cenários pré-definidos
|
│ └── presets.ts # Cenários pré-definidos (processos e threads)
|
||||||
│
|
│
|
||||||
├── store/ # Estado global com Zustand
|
├── store/ # Estado global com Zustand
|
||||||
├── components/ # Componentes React
|
├── components/ # Componentes React
|
||||||
│ ├── ProcessPanel/ # Criação e edição de processos
|
│ ├── ProcessPanel/ # Criação e edição de processos
|
||||||
|
│ │ ├── ThreadEditor.tsx # Editor de threads (modelo, política, bursts por thread)
|
||||||
|
│ │ └── ...
|
||||||
│ ├── SchedulerPanel/ # Seleção de algoritmo e parâmetros
|
│ ├── SchedulerPanel/ # Seleção de algoritmo e parâmetros
|
||||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
|
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
|
||||||
│ ├── GanttChart/ # Diagrama de Gantt interativo
|
│ ├── GanttChart/ # Diagrama de Gantt interativo
|
||||||
|
│ │ ├── GanttThreadRow.tsx # Sub-linha de thread no Gantt
|
||||||
|
│ │ └── ...
|
||||||
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
|
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
|
||||||
|
│ │ ├── ThreadMetricsTable.tsx # Tabela de métricas por thread
|
||||||
|
│ │ └── ...
|
||||||
│ └── shared/ # Botões, badges, tooltips, modais
|
│ └── shared/ # Botões, badges, tooltips, modais
|
||||||
|
│ ├── threadUtils.ts # Derivação de cores de threads (HSL)
|
||||||
|
│ └── ...
|
||||||
│
|
│
|
||||||
└── hooks/ # Hooks personalizados (loop de animação, export, etc.)
|
└── hooks/ # Hooks personalizados (loop de animação, export, etc.)
|
||||||
```
|
```
|
||||||
@@ -141,6 +180,8 @@ src/
|
|||||||
|
|
||||||
Carregue cenários clássicos com um clique para explorar fenômenos de SO:
|
Carregue cenários clássicos com um clique para explorar fenômenos de SO:
|
||||||
|
|
||||||
|
**Processos:**
|
||||||
|
|
||||||
| Preset | O que demonstra |
|
| Preset | O que demonstra |
|
||||||
|--------|-----------------|
|
|--------|-----------------|
|
||||||
| `classic_fcfs` | Execução básica com FCFS, sem I/O |
|
| `classic_fcfs` | Execução básica com FCFS, sem I/O |
|
||||||
@@ -151,6 +192,16 @@ Carregue cenários clássicos com um clique para explorar fenômenos de SO:
|
|||||||
| `io_heavy` | Mix de processos CPU Bound vs I/O Bound |
|
| `io_heavy` | Mix de processos CPU Bound vs I/O Bound |
|
||||||
| `multilevel_demo` | Múltiplas filas com prioridades diferentes |
|
| `multilevel_demo` | Múltiplas filas com prioridades diferentes |
|
||||||
|
|
||||||
|
**Threads:**
|
||||||
|
|
||||||
|
| Preset | O que demonstra |
|
||||||
|
|--------|-----------------|
|
||||||
|
| `many_to_one` | Modelo N:1 — I/O de uma thread bloqueia todo o processo |
|
||||||
|
| `one_to_one` | Modelo 1:1 — threads bloqueiam independentemente |
|
||||||
|
| `many_to_many` | Modelo N:M — slots de kernel limitam o paralelismo |
|
||||||
|
| `thread_starvation` | Starvation intra-processo com política Priority |
|
||||||
|
| `threads_vs_processes` | Comparação direta: processo com threads vs processo único |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Testes
|
## 🧪 Testes
|
||||||
@@ -187,9 +238,9 @@ Os testes cobrem a engine de simulação, todos os algoritmos de escalonamento,
|
|||||||
|
|
||||||
## 📄 Documentação adicional
|
## 📄 Documentação adicional
|
||||||
|
|
||||||
- [📐 Algoritmos de Escalonamento](./docs/algorithms.md) — descrição detalhada de cada algoritmo, regras de desempate e exemplos
|
- [📐 Algoritmos de Escalonamento](./docs/algorithms.md): descrição detalhada de cada algoritmo, regras de desempate e exemplos
|
||||||
- [📏 Métricas](./docs/metrics.md) — fórmulas e definições de todas as métricas calculadas
|
- [📏 Métricas](./docs/metrics.md): fórmulas e definições de todas as métricas calculadas
|
||||||
- [📝 JOSS Paper](./paper.md) — artigo científico submetido ao Journal of Open Source Software
|
- [📝 JOSS Paper](./paper.md): artigo científico submetido ao Journal of Open Source Software
|
||||||
|
|
||||||
*Also available in: [🇺🇸 English](./README.en.md)*
|
*Also available in: [🇺🇸 English](./README.en.md)*
|
||||||
|
|
||||||
@@ -203,4 +254,4 @@ Contribuições são bem-vindas! Por favor, consulte nosso guia em [CONTRIBUTING
|
|||||||
|
|
||||||
## 📜 Licença
|
## 📜 Licença
|
||||||
|
|
||||||
Distribuído sob a [Licença MIT](./LICENSE). Desenvolvido por [Lucas Rayan Guerra](https://github.com/lucasrguerra).
|
Distribuído sob a [Licença MIT](./LICENSE). Desenvolvido por [Lucas Rayan Guerra](https://git.lucasrguerra.dev.br/lucasrguerra).
|
||||||
BIN
Binary file not shown.
+7
-1
@@ -2,5 +2,11 @@ services:
|
|||||||
pss:
|
pss:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-3000}:80"
|
- "${PORT:-3000}:8080"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
+110
-10
@@ -15,13 +15,13 @@ This document describes in detail the scheduling algorithms implemented in PSS.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FCFS — First Come, First Served
|
## FCFS (First Come, First Served)
|
||||||
|
|
||||||
**Type:** Non-preemptive
|
**Type:** Non-preemptive
|
||||||
|
|
||||||
### How it works
|
### How it works
|
||||||
|
|
||||||
Perhaps the most intuitive algorithm: the process that arrived first is served first. The Ready queue is sorted by `arrivalTime`. Once a process occupies the CPU, it stays there until its current CPU burst is complete — no interruptions.
|
Perhaps the most intuitive algorithm: the process that arrived first is served first. The Ready queue is sorted by `arrivalTime`. Once a process occupies the CPU, it stays there until its current CPU burst is complete, with no interruptions.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@ Tick: 0 1 2 3 4 5 6 7
|
|||||||
|
|
||||||
### Note: Convoy Effect
|
### Note: Convoy Effect
|
||||||
|
|
||||||
If a long process arrives before several short ones, the short ones wait a long time — even though the CPU could be serving them. This phenomenon is called the **Convoy Effect** and is one of the main drawbacks of FCFS. Try the `convoy_effect` preset to visualize it.
|
If a long process arrives before several short ones, the short ones wait a long time, even though the CPU could be serving them. This phenomenon is called the **Convoy Effect** and is one of the main drawbacks of FCFS. Try the `convoy_effect` preset to visualize it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SJF — Shortest Job First (Non-Preemptive)
|
## SJF (Shortest Job First - Non-Preemptive)
|
||||||
|
|
||||||
**Type:** Non-preemptive
|
**Type:** Non-preemptive
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ No additional parameters.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SRTF — Shortest Remaining Time First (Preemptive SJF)
|
## SRTF (Shortest Remaining Time First - Preemptive SJF)
|
||||||
|
|
||||||
**Type:** Preemptive
|
**Type:** Preemptive
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ Tick 12: P1 finishes
|
|||||||
|
|
||||||
The Ready queue is treated as a circular queue. Each process receives at most `quantum` consecutive CPU ticks. If the process still has remaining burst at the end of the quantum, it goes back to the **end** of the Ready queue and waits its turn.
|
The Ready queue is treated as a circular queue. Each process receives at most `quantum` consecutive CPU ticks. If the process still has remaining burst at the end of the quantum, it goes back to the **end** of the Ready queue and waits its turn.
|
||||||
|
|
||||||
I/O bursts **do not** consume quantum — the process voluntarily leaves the CPU when initiating I/O.
|
I/O bursts **do not** consume quantum, as the process voluntarily leaves the CPU when initiating I/O.
|
||||||
|
|
||||||
### Trade-off
|
### Trade-off
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ Try the `starvation` and `aging_fix` presets to see this difference.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## HRRN — Highest Response Ratio Next
|
## HRRN (Highest Response Ratio Next)
|
||||||
|
|
||||||
**Type:** Non-preemptive
|
**Type:** Non-preemptive
|
||||||
|
|
||||||
@@ -152,13 +152,113 @@ The process with the **highest ratio** is selected. This naturally increases the
|
|||||||
|
|
||||||
### How it works
|
### How it works
|
||||||
|
|
||||||
Processes are distributed across multiple queues with different priorities. Each queue has its own Round Robin quantum. Higher-priority queues are always served first — a lower-priority queue only receives CPU if all queues above it are empty.
|
Processes are distributed across multiple queues with different priorities. Each queue has its own Round Robin quantum. Higher-priority queues are always served first. A lower-priority queue only receives CPU if all queues above it are empty.
|
||||||
|
|
||||||
This allows different classes of processes to be treated distinctly: for example, interactive processes in high-priority queues (small quantum) and batch processes in low-priority queues (larger quantum).
|
This allows different classes of processes to be treated distinctly: for example, interactive processes in high-priority queues (small quantum) and batch processes in low-priority queues (larger quantum).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## MLFQ (Multilevel Feedback Queue)
|
||||||
|
|
||||||
|
**Type:** Preemptive
|
||||||
|
**Parameters:** number of queues, quantum per queue (default: [2, 4, 8]), boost interval
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
MLFQ is an extension of the Multilevel Queue that allows **processes to move between queues** based on their observed runtime behavior — eliminating the need to know burst durations in advance.
|
||||||
|
|
||||||
|
#### Movement rules
|
||||||
|
|
||||||
|
| Event | Action |
|
||||||
|
|-------|--------|
|
||||||
|
| Newly arrived process | Enters **queue 0** (highest priority) |
|
||||||
|
| Process exhausts the quantum of its current queue | **Demoted** to the next queue (lower priority) |
|
||||||
|
| Process returns from I/O | Stays in the **same queue** (cooperative behavior is rewarded) |
|
||||||
|
| Boost tick (configurable interval) | **All** processes are moved to queue 0 |
|
||||||
|
|
||||||
|
#### Why is MLFQ efficient?
|
||||||
|
|
||||||
|
- Interactive processes (I/O Bound) naturally stay in high-priority queues because they leave the CPU before exhausting the quantum.
|
||||||
|
- Batch processes (CPU Bound) progressively migrate to lower-priority queues where they receive larger quantums.
|
||||||
|
- The periodic boost prevents starvation: even processes that fell to lower queues eventually return to the top.
|
||||||
|
|
||||||
|
#### Example with 3 queues (quantums: 2, 4, 8)
|
||||||
|
|
||||||
|
```
|
||||||
|
Process A (CPU Bound, burst=12):
|
||||||
|
Queue 0 → uses 2 ticks, demoted → Queue 1
|
||||||
|
Queue 1 → uses 4 ticks, demoted → Queue 2
|
||||||
|
Queue 2 → uses remaining 6 ticks (quantum=8, not exhausted)
|
||||||
|
|
||||||
|
Process B (I/O Bound, burst=1 CPU + I/O + burst=1 CPU):
|
||||||
|
Queue 0 → uses 1 tick → goes to I/O → returns to Queue 0
|
||||||
|
→ always stays in the highest-priority queue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| Number of queues | 3 | Number of priority queues |
|
||||||
|
| Quantum per queue | [2, 4, 8] | Time slice for each queue |
|
||||||
|
| Boost interval | 20 ticks | Every N ticks, all processes move up to queue 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thread Models
|
||||||
|
|
||||||
|
PSS supports **thread** simulation within processes, implementing the three classical models described in Operating Systems textbooks.
|
||||||
|
|
||||||
|
### General Concepts
|
||||||
|
|
||||||
|
- A process can have **N user threads**, each with its own burst sequence (CPU and I/O).
|
||||||
|
- The scheduling policy among threads within the same process is configurable: **FCFS**, **Round Robin**, or **Priority**.
|
||||||
|
- The thread model determines how user threads map to kernel threads.
|
||||||
|
|
||||||
|
### MANY_TO_ONE (N:1)
|
||||||
|
|
||||||
|
**N user threads → 1 kernel thread**
|
||||||
|
|
||||||
|
- Only one thread can use the CPU at a time within the process.
|
||||||
|
- If any thread initiates I/O, **the entire process blocks** (the kernel thread is stuck).
|
||||||
|
- No real parallelism; very simple to implement.
|
||||||
|
- Simulated by the `many_to_one` presets.
|
||||||
|
|
||||||
|
### ONE_TO_ONE (1:1)
|
||||||
|
|
||||||
|
**Each user thread → 1 dedicated kernel thread**
|
||||||
|
|
||||||
|
- Threads block **independently**: if Thread A does I/O, Threads B and C remain in the Ready queue.
|
||||||
|
- The process only terminates when **all** threads have terminated.
|
||||||
|
- The process blocks only when all threads are simultaneously in I/O.
|
||||||
|
- Simulated by the `one_to_one` presets.
|
||||||
|
|
||||||
|
### MANY_TO_MANY (N:M)
|
||||||
|
|
||||||
|
**N user threads → M kernel threads (M ≤ N)**
|
||||||
|
|
||||||
|
- The number of kernel threads (`kernelThreadCount`) is configurable.
|
||||||
|
- If all M kernel slots are occupied, additional user threads wait in the **kernel wait queue** (`KernelWait`).
|
||||||
|
- Combines flexibility with resource control: more active threads than N:1 but less overhead than 1:1.
|
||||||
|
- Simulated by the `many_to_many` presets.
|
||||||
|
|
||||||
|
### Intra-Process Policy
|
||||||
|
|
||||||
|
Defines how the scheduler selects the next thread when multiple threads are Ready within the same process:
|
||||||
|
|
||||||
|
| Policy | Behavior |
|
||||||
|
|--------|----------|
|
||||||
|
| **FCFS** | The oldest thread in the Ready queue is selected |
|
||||||
|
| **Round Robin (RR)** | Each thread receives `threadQuantum` ticks; when exhausted, it goes to the end of the queue |
|
||||||
|
| **Priority** | The highest-priority thread is selected; immediate preemption if a higher-priority thread becomes ready |
|
||||||
|
|
||||||
|
### Gantt Chart with Threads
|
||||||
|
|
||||||
|
When a process has threads, the Gantt chart displays **sub-rows** below the process row, one per thread, with colors derived from the process color (40° hue shift per thread). The state of each thread (New, Ready, Running, Waiting, Terminated) is colored using the same CSS variables as process states.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Silberschatz, A., Galvin, P. B., & Gagne, G. — *Operating System Concepts*, 10th edition
|
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
|
||||||
- Tanenbaum, A. S. — *Modern Operating Systems*, 4th edition
|
- Tanenbaum, A. S., *Modern Operating Systems*, 4th edition.
|
||||||
+110
-10
@@ -15,13 +15,13 @@ Este documento descreve em detalhes os algoritmos de escalonamento implementados
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FCFS — First Come, First Served
|
## FCFS (First Come, First Served)
|
||||||
|
|
||||||
**Tipo:** Não-preemptivo
|
**Tipo:** Não-preemptivo
|
||||||
|
|
||||||
### Como funciona
|
### Como funciona
|
||||||
|
|
||||||
Talvez o algoritmo mais intuitivo: o processo que chegou primeiro é atendido primeiro. A fila Ready é ordenada por `arrivalTime`. Quando um processo ocupa a CPU, ele permanece lá até completar seu burst de CPU atual — sem interrupções.
|
Talvez o algoritmo mais intuitivo: o processo que chegou primeiro é atendido primeiro. A fila Ready é ordenada por `arrivalTime`. Quando um processo ocupa a CPU, ele permanece lá até completar seu burst de CPU atual, sem interrupções.
|
||||||
|
|
||||||
### Exemplo
|
### Exemplo
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@ Tick: 0 1 2 3 4 5 6 7
|
|||||||
|
|
||||||
### Observação: Efeito de Comboio
|
### Observação: Efeito de Comboio
|
||||||
|
|
||||||
Se um processo longo chega antes de vários processos curtos, estes ficam esperando por muito tempo — mesmo que a CPU esteja "disponível" para eles. Este fenômeno é chamado de **Convoy Effect** e é um dos principais problemas do FCFS. Experimente o preset `convoy_effect` para visualizá-lo.
|
Se um processo longo chega antes de vários processos curtos, estes ficam esperando por muito tempo, mesmo que a CPU esteja "disponível" para eles. Este fenômeno é chamado de **Convoy Effect** e é um dos principais problemas do FCFS. Experimente o preset `convoy_effect` para visualizá-lo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SJF — Shortest Job First (Não-Preemptivo)
|
## SJF (Shortest Job First - Não-Preemptivo)
|
||||||
|
|
||||||
**Tipo:** Não-preemptivo
|
**Tipo:** Não-preemptivo
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Nenhum parâmetro adicional.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SRTF — Shortest Remaining Time First (SJF Preemptivo)
|
## SRTF (Shortest Remaining Time First - SJF Preemptivo)
|
||||||
|
|
||||||
**Tipo:** Preemptivo
|
**Tipo:** Preemptivo
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ Tick 12: P1 termina
|
|||||||
|
|
||||||
A fila Ready é tratada como uma fila circular. Cada processo recebe no máximo `quantum` ticks de CPU consecutivos. Se ao final do quantum o processo ainda tiver burst restante, ele retorna ao **fim** da fila Ready e aguarda sua vez.
|
A fila Ready é tratada como uma fila circular. Cada processo recebe no máximo `quantum` ticks de CPU consecutivos. Se ao final do quantum o processo ainda tiver burst restante, ele retorna ao **fim** da fila Ready e aguarda sua vez.
|
||||||
|
|
||||||
I/O bursts **não** consomem quantum — o processo sai voluntariamente da CPU quando inicia I/O.
|
I/O bursts **não** consomem quantum, pois o processo sai voluntariamente da CPU quando inicia I/O.
|
||||||
|
|
||||||
### Tradeoff
|
### Tradeoff
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ Experimente os presets `starvation` e `aging_fix` para ver esta diferença.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## HRRN — Highest Response Ratio Next
|
## HRRN (Highest Response Ratio Next)
|
||||||
|
|
||||||
**Tipo:** Não-preemptivo
|
**Tipo:** Não-preemptivo
|
||||||
|
|
||||||
@@ -152,13 +152,113 @@ O processo com o **maior ratio** é selecionado. Isso faz com que processos que
|
|||||||
|
|
||||||
### Como funciona
|
### Como funciona
|
||||||
|
|
||||||
Os processos são distribuídos em múltiplas filas com diferentes prioridades. Cada fila tem seu próprio quantum de Round Robin. Filas de maior prioridade sempre são servidas primeiro — uma fila de menor prioridade só recebe CPU se todas as filas acima dela estiverem vazias.
|
Os processos são distribuídos em múltiplas filas com diferentes prioridades. Cada fila tem seu próprio quantum de Round Robin. Filas de maior prioridade sempre são servidas primeiro. Uma fila de menor prioridade só recebe CPU se todas as filas acima dela estiverem vazias.
|
||||||
|
|
||||||
Isso permite tratar diferentes classes de processos de forma distinta: por exemplo, processos interativos em filas de alta prioridade (quantum pequeno) e processos batch em filas de baixa prioridade (quantum maior).
|
Isso permite tratar diferentes classes de processos de forma distinta: por exemplo, processos interativos em filas de alta prioridade (quantum pequeno) e processos batch em filas de baixa prioridade (quantum maior).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## MLFQ (Multilevel Feedback Queue)
|
||||||
|
|
||||||
|
**Tipo:** Preemptivo
|
||||||
|
**Parâmetros:** número de filas, quantum por fila (padrão: [2, 4, 8]), intervalo de boost
|
||||||
|
|
||||||
|
### Como funciona
|
||||||
|
|
||||||
|
O MLFQ é uma extensão do Multilevel Queue que permite **mover processos entre filas** com base em seu comportamento observado em tempo de execução — eliminando a necessidade de conhecer os bursts antecipadamente.
|
||||||
|
|
||||||
|
#### Regras de movimentação
|
||||||
|
|
||||||
|
| Evento | Ação |
|
||||||
|
|--------|------|
|
||||||
|
| Processo recém-chegado | Entra na **fila 0** (maior prioridade) |
|
||||||
|
| Processo esgota o quantum da fila atual | **Rebaixado** para a fila seguinte (menor prioridade) |
|
||||||
|
| Processo retorna do I/O | Permanece na **mesma fila** (comportamento cooperativo é recompensado) |
|
||||||
|
| Tick de boost (intervalo configurável) | **Todos** os processos são movidos para a fila 0 |
|
||||||
|
|
||||||
|
#### Por que o MLFQ é eficiente?
|
||||||
|
|
||||||
|
- Processos interativos (I/O Bound) naturalmente ficam nas filas de alta prioridade porque abandonam a CPU antes de esgotar o quantum.
|
||||||
|
- Processos batch (CPU Bound) migram progressivamente para filas de baixa prioridade onde recebem quantuns maiores.
|
||||||
|
- O boost periódico evita starvation: mesmo processos que caíram para as filas inferiores eventualmente retornam ao topo.
|
||||||
|
|
||||||
|
#### Exemplo com 3 filas (quantuns: 2, 4, 8)
|
||||||
|
|
||||||
|
```
|
||||||
|
Processo A (CPU Bound, burst=12):
|
||||||
|
Fila 0 → usa 2 ticks, rebaixado → Fila 1
|
||||||
|
Fila 1 → usa 4 ticks, rebaixado → Fila 2
|
||||||
|
Fila 2 → usa 6 ticks restantes (quantum=8, não esgota)
|
||||||
|
|
||||||
|
Processo B (I/O Bound, burst=1 CPU + I/O + burst=1 CPU):
|
||||||
|
Fila 0 → usa 1 tick → vai para I/O → retorna à Fila 0
|
||||||
|
→ sempre permanece na fila de maior prioridade
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parâmetros
|
||||||
|
|
||||||
|
| Parâmetro | Padrão | Descrição |
|
||||||
|
|-----------|--------|-----------|
|
||||||
|
| Número de filas | 3 | Quantidade de filas de prioridade |
|
||||||
|
| Quantuns por fila | [2, 4, 8] | Fatia de tempo de cada fila |
|
||||||
|
| Intervalo de boost | 20 ticks | A cada N ticks, todos os processos sobem para a fila 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modelos de Threads
|
||||||
|
|
||||||
|
O PSS suporta simulação de **threads** dentro de processos, implementando os três modelos clássicos descritos em Sistemas Operacionais.
|
||||||
|
|
||||||
|
### Conceitos Gerais
|
||||||
|
|
||||||
|
- Um processo pode ter **N threads de usuário**, cada uma com sua própria sequência de bursts (CPU e I/O).
|
||||||
|
- A política de escalonamento entre threads de um mesmo processo é configurável: **FCFS**, **Round Robin** ou **Prioridade**.
|
||||||
|
- O modelo de thread determina como threads de usuário se mapeiam para threads de kernel.
|
||||||
|
|
||||||
|
### MANY_TO_ONE (N:1)
|
||||||
|
|
||||||
|
**N threads de usuário → 1 thread de kernel**
|
||||||
|
|
||||||
|
- Apenas uma thread pode usar a CPU por vez dentro do processo.
|
||||||
|
- Se qualquer thread iniciar I/O, **todo o processo bloqueia** (a thread de kernel fica presa).
|
||||||
|
- Sem paralelismo real; muito simples de implementar.
|
||||||
|
- Simulado pelos presets `many_to_one`.
|
||||||
|
|
||||||
|
### ONE_TO_ONE (1:1)
|
||||||
|
|
||||||
|
**Cada thread de usuário → 1 thread de kernel dedicada**
|
||||||
|
|
||||||
|
- Threads bloqueiam **independentemente**: se a Thread A faz I/O, as Threads B e C continuam na fila Ready.
|
||||||
|
- Processo só termina quando **todas** as threads terminam.
|
||||||
|
- Processo bloqueia somente quando todas as threads estão simultâneamente em I/O.
|
||||||
|
- Simulado pelos presets `one_to_one`.
|
||||||
|
|
||||||
|
### MANY_TO_MANY (N:M)
|
||||||
|
|
||||||
|
**N threads de usuário → M threads de kernel (M ≤ N)**
|
||||||
|
|
||||||
|
- O número de threads de kernel (`kernelThreadCount`) é configurável.
|
||||||
|
- Se todas as M slots de kernel estiverem ocupadas, threads de usuário adicionais ficam em **fila de espera de kernel** (`KernelWait`).
|
||||||
|
- Combina flexibilidade com controle de recursos: mais threads ativas que N:1 mas menos overhead que 1:1.
|
||||||
|
- Simulado pelos presets `many_to_many`.
|
||||||
|
|
||||||
|
### Política Intra-Processo
|
||||||
|
|
||||||
|
Define como o escalonador seleciona a próxima thread quando há múltiplas threads Ready no mesmo processo:
|
||||||
|
|
||||||
|
| Política | Comportamento |
|
||||||
|
|----------|--------------|
|
||||||
|
| **FCFS** | Thread mais antiga na fila Ready é selecionada |
|
||||||
|
| **Round Robin (RR)** | Cada thread recebe `threadQuantum` ticks; ao esgotar, vai para o fim da fila |
|
||||||
|
| **Priority** | Thread de maior prioridade é selecionada; preempção imediata se nova thread de maior prioridade chegar |
|
||||||
|
|
||||||
|
### Diagrama de Gantt com Threads
|
||||||
|
|
||||||
|
Quando um processo possui threads, o Gantt exibe **sub-linhas** abaixo da linha do processo, uma por thread, com cores derivadas da cor do processo (deslocamento de matiz de 40° por thread). O estado de cada thread (New, Ready, Running, Waiting, Terminated) é colorido com as mesmas variáveis CSS dos processos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Referências
|
## Referências
|
||||||
|
|
||||||
- Silberschatz, A., Galvin, P. B., & Gagne, G. — *Operating System Concepts*, 10ª edição
|
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10ª edição.
|
||||||
- Tanenbaum, A. S. — *Modern Operating Systems*, 4ª edição
|
- Tanenbaum, A. S., *Modern Operating Systems*, 4ª edição.
|
||||||
+41
-6
@@ -79,7 +79,7 @@ otherwise → Balanced
|
|||||||
|
|
||||||
## Charts ("Chart" tab)
|
## Charts ("Chart" tab)
|
||||||
|
|
||||||
### Comparative Profile — Radar Chart
|
### Comparative Profile (Radar Chart)
|
||||||
|
|
||||||
Displayed when there are **2 or more processes**. Each axis represents a time dimension, normalized from 0–100% relative to the largest value in that dimension among all processes:
|
Displayed when there are **2 or more processes**. Each axis represents a time dimension, normalized from 0–100% relative to the largest value in that dimension among all processes:
|
||||||
|
|
||||||
@@ -91,20 +91,55 @@ Displayed when there are **2 or more processes**. Each axis represents a time di
|
|||||||
| **CPU Time** | Relative CPU usage |
|
| **CPU Time** | Relative CPU usage |
|
||||||
| **I/O Time** | Relative I/O usage |
|
| **I/O Time** | Relative I/O usage |
|
||||||
|
|
||||||
Each process is drawn as a colored polygon (the color configured for the process). Wider polygons indicate processes with larger times in the corresponding dimensions. The radar chart is ideal for identifying **profile differences** between processes — for example, a CPU Bound process will have a much larger CPU Time axis than I/O Time.
|
Each process is drawn as a colored polygon (the color configured for the process). Wider polygons indicate processes with larger times in the corresponding dimensions. The radar chart is ideal for identifying **profile differences** between processes (for example, a CPU Bound process will have a much larger CPU Time axis than I/O Time).
|
||||||
|
|
||||||
### Per-Process Breakdown — Stacked Bar Chart
|
### Per-Process Breakdown (Stacked Bar Chart)
|
||||||
|
|
||||||
Always displayed. Each bar represents a process, with stacked segments:
|
Always displayed. Each bar represents a process, with stacked segments:
|
||||||
|
|
||||||
- **Amber** — Waiting Time
|
- **Amber**: Waiting Time
|
||||||
- **Green** — CPU Time
|
- **Green**: CPU Time
|
||||||
- **Cyan** — I/O Time
|
- **Cyan**: I/O Time
|
||||||
|
|
||||||
The total height of each bar equals the **Turnaround Time** of the process.
|
The total height of each bar equals the **Turnaround Time** of the process.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Thread Metrics
|
||||||
|
|
||||||
|
When a process has threads, PSS computes individual metrics for each thread. These metrics are displayed in the **Threads** tab of the metrics panel (visible only when there are processes with threads).
|
||||||
|
|
||||||
|
### Per-Thread Metrics
|
||||||
|
|
||||||
|
| Metric | Formula | What it means |
|
||||||
|
|--------|---------|----------------|
|
||||||
|
| **Arrival Time** | `arrivalTick` | Tick at which the thread first entered the Ready queue |
|
||||||
|
| **Start** | `startTick` | Tick at which the thread first used the CPU |
|
||||||
|
| **Finish** | `finishTick` | Tick at which the thread completed all of its bursts |
|
||||||
|
| **Response Time** | `startTick − arrivalTick` | How long the thread waited before receiving the CPU for the first time |
|
||||||
|
| **Turnaround Time** | `finishTick − arrivalTick` | Total lifetime of the thread (arrival → completion) |
|
||||||
|
| **Waiting Time** | `turnaround − cpuTime − ioTime` | Time in the Ready queue without doing anything useful |
|
||||||
|
| **CPU Time** | Σ ticks in Running state | Total CPU consumed by the thread |
|
||||||
|
| **I/O Time** | Σ ticks in Waiting state | Total time waiting for I/O |
|
||||||
|
|
||||||
|
### Thread Models and their impact on metrics
|
||||||
|
|
||||||
|
The thread model directly affects the recorded times:
|
||||||
|
|
||||||
|
| Model | Impact on Waiting Time |
|
||||||
|
|-------|------------------------|
|
||||||
|
| **MANY_TO_ONE** | Waiting Time tends to be high: any thread's I/O blocks the entire process, forcing other threads to wait |
|
||||||
|
| **ONE_TO_ONE** | Lower Waiting Time: threads block independently; while one does I/O, the others remain in the Ready queue |
|
||||||
|
| **MANY_TO_MANY** | Intermediate Waiting Time: threads that exceed the kernel slots enter `KernelWait` (shown with reduced opacity in the Gantt chart) |
|
||||||
|
|
||||||
|
### Visualization
|
||||||
|
|
||||||
|
The thread table displays inline horizontal mini progress bars in the Response, Turnaround, Waiting, CPU, and I/O columns, normalized by the largest value in each column across all threads — just like the process metrics table.
|
||||||
|
|
||||||
|
Each thread has a color derived from the parent process color (40° HSL hue shift per thread), shown as a colored dot in the table and as a sub-row in the Gantt chart.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Consider three processes with the following bursts:
|
Consider three processes with the following bursts:
|
||||||
|
|||||||
+41
-6
@@ -79,7 +79,7 @@ caso contrário → Balanced
|
|||||||
|
|
||||||
## Gráficos (aba "Gráfico")
|
## Gráficos (aba "Gráfico")
|
||||||
|
|
||||||
### Perfil Comparativo — Radar Chart
|
### Perfil Comparativo (Radar Chart)
|
||||||
|
|
||||||
Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão de tempo, normalizada de 0–100% em relação ao maior valor dessa dimensão entre todos os processos:
|
Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão de tempo, normalizada de 0–100% em relação ao maior valor dessa dimensão entre todos os processos:
|
||||||
|
|
||||||
@@ -91,20 +91,55 @@ Exibido quando há **2 ou mais processos**. Cada eixo representa uma dimensão d
|
|||||||
| **CPU Time** | Uso de CPU relativo |
|
| **CPU Time** | Uso de CPU relativo |
|
||||||
| **I/O Time** | Uso de I/O relativo |
|
| **I/O Time** | Uso de I/O relativo |
|
||||||
|
|
||||||
Cada processo é desenhado como um polígono colorido (a cor configurada no processo). Polígonos mais "amplos" indicam processos com tempos maiores nas dimensões correspondentes. O radar é ideal para identificar **diferenças de perfil** entre processos — por exemplo, um processo CPU Bound terá o eixo CPU Time muito maior que o eixo I/O Time.
|
Cada processo é desenhado como um polígono colorido (a cor configurada no processo). Polígonos mais "amplos" indicam processos com tempos maiores nas dimensões correspondentes. O radar é ideal para identificar **diferenças de perfil** entre processos (por exemplo, um processo CPU Bound terá o eixo CPU Time muito maior que o eixo I/O Time).
|
||||||
|
|
||||||
### Breakdown por Processo — Stacked Bar Chart
|
### Breakdown por Processo (Stacked Bar Chart)
|
||||||
|
|
||||||
Sempre exibido. Cada barra representa um processo, com os segmentos empilhados:
|
Sempre exibido. Cada barra representa um processo, com os segmentos empilhados:
|
||||||
|
|
||||||
- **Âmbar** — Waiting Time
|
- **Âmbar**: Waiting Time
|
||||||
- **Verde** — CPU Time
|
- **Verde**: CPU Time
|
||||||
- **Ciano** — I/O Time
|
- **Ciano**: I/O Time
|
||||||
|
|
||||||
A altura total de cada barra equivale ao **Turnaround Time** do processo.
|
A altura total de cada barra equivale ao **Turnaround Time** do processo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Métricas de Threads
|
||||||
|
|
||||||
|
Quando um processo possui threads, o PSS calcula métricas individuais para cada thread. Essas métricas são exibidas na aba **Threads** do painel de métricas (visível apenas quando há processos com threads).
|
||||||
|
|
||||||
|
### Métricas por Thread
|
||||||
|
|
||||||
|
| Métrica | Fórmula | O que significa |
|
||||||
|
|---------|---------|----------------|
|
||||||
|
| **Arrival Time** | `arrivalTick` | Tick em que a thread entrou pela primeira vez na fila Ready |
|
||||||
|
| **Start** | `startTick` | Tick em que a thread usou a CPU pela primeira vez |
|
||||||
|
| **Finish** | `finishTick` | Tick em que a thread encerrou todos os seus bursts |
|
||||||
|
| **Response Time** | `startTick − arrivalTick` | Quanto a thread esperou antes de receber a CPU pela primeira vez |
|
||||||
|
| **Turnaround Time** | `finishTick − arrivalTick` | Tempo total de vida da thread (chegada → término) |
|
||||||
|
| **Waiting Time** | `turnaround − cpuTime − ioTime` | Tempo na fila Ready sem executar nada útil |
|
||||||
|
| **CPU Time** | Σ ticks em estado Running | Total de CPU consumido pela thread |
|
||||||
|
| **I/O Time** | Σ ticks em estado Waiting | Total de tempo aguardando I/O |
|
||||||
|
|
||||||
|
### Modelos de Thread e seu impacto nas métricas
|
||||||
|
|
||||||
|
O modelo de thread afeta diretamente os tempos registrados:
|
||||||
|
|
||||||
|
| Modelo | Impacto no Waiting Time |
|
||||||
|
|--------|------------------------|
|
||||||
|
| **MANY_TO_ONE** | Waiting Time tende a ser alto: qualquer I/O de uma thread bloqueia todo o processo, forçando as demais threads a esperarem |
|
||||||
|
| **ONE_TO_ONE** | Waiting Time menor: threads bloqueiam independentemente; enquanto uma faz I/O, as outras continuam na fila Ready |
|
||||||
|
| **MANY_TO_MANY** | Waiting Time intermediário: threads que excedem os slots de kernel ficam em `KernelWait` (indicadas com `opacity` reduzida no Gantt) |
|
||||||
|
|
||||||
|
### Visualização
|
||||||
|
|
||||||
|
A tabela de threads exibe mini barras de progresso horizontais nas colunas de Response, Turnaround, Waiting, CPU e I/O, normalizadas pelo maior valor de cada coluna entre todas as threads — assim como na tabela de processos.
|
||||||
|
|
||||||
|
Cada thread possui uma cor derivada da cor do processo pai (deslocamento de matiz HSL de 40° por thread), exibida como ponto colorido na tabela e como sub-linha no Gantt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Exemplo
|
## Exemplo
|
||||||
|
|
||||||
Considere três processos com os seguintes bursts:
|
Considere três processos com os seguintes bursts:
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="pt-BR">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="Process Scheduler Simulator — interactive browser-based tool for visualizing CPU scheduling algorithms" />
|
||||||
<title>Process Scheduler Simulator</title>
|
<title>Process Scheduler Simulator</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-900 text-slate-100 antialiased overflow-hidden selection:bg-blue-500/30">
|
<body class="bg-slate-900 text-slate-100 antialiased overflow-hidden selection:bg-blue-500/30">
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 8080;
|
||||||
server_name _;
|
server_name _;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|||||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "pss",
|
"name": "pss",
|
||||||
"version": "1.0.0",
|
"version": "1.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pss",
|
"name": "pss",
|
||||||
"version": "1.0.0",
|
"version": "1.3.3",
|
||||||
"license": "ISC",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
|||||||
+5
-6
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "pss",
|
"name": "pss",
|
||||||
"version": "1.1.0",
|
"version": "1.3.3",
|
||||||
"description": "O Process Scheduler Simulator é uma ferramenta educacional interativa que roda inteiramente no navegador",
|
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"build:release": "tsc -b && vite build && tar --dereference -czf dist.tar.gz dist",
|
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
@@ -39,14 +38,14 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/lucasrguerra/pss.git"
|
"url": "git+https://git.lucasrguerra.dev.br/lucasrguerra/pss.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Lucas Rayan Guerra",
|
"author": "Lucas Rayan Guerra",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/lucasrguerra/pss/issues"
|
"url": "https://git.lucasrguerra.dev.br/lucasrguerra/pss/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/lucasrguerra/pss#readme"
|
"homepage": "https://git.lucasrguerra.dev.br/lucasrguerra/pss#readme"
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ bibliography: references.bib
|
|||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
The **Process Scheduler Simulator (PSS)** is an open-source, browser-based interactive tool designed to help students, instructors, and practitioners understand and compare classical CPU scheduling algorithms from the field of Operating Systems (OS). PSS runs entirely client-side — no server, no installation — making it instantly accessible via any modern web browser at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br).
|
The **Process Scheduler Simulator (PSS)** is an open-source, browser-based interactive tool designed to help students, instructors, and practitioners understand and compare classical CPU scheduling algorithms from the field of Operating Systems (OS). PSS runs entirely client-side, requiring no server or installation, which makes it instantly accessible via any modern web browser at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br).
|
||||||
|
|
||||||
Users define a set of processes, each with an arbitrary sequence of alternating CPU and I/O bursts, and select one of eight scheduling algorithms. The simulator then executes the schedule tick by tick, rendering a live Gantt chart and computing a comprehensive set of performance metrics for each process and for the simulation as a whole. PSS supports First Come First Served (FCFS), Shortest Job First (SJF), Shortest Remaining Time First (SRTF), Round Robin (RR), non-preemptive and preemptive Priority Scheduling (with optional aging), Highest Response Ratio Next (HRRN), and Multilevel Queue scheduling.
|
Users define a set of processes, each with an arbitrary sequence of alternating CPU and I/O bursts, and select one of eight scheduling algorithms. The simulator then executes the schedule tick by tick, rendering a live Gantt chart and computing a comprehensive set of performance metrics for each process and for the simulation as a whole. PSS supports First Come First Served (FCFS), Shortest Job First (SJF), Shortest Remaining Time First (SRTF), Round Robin (RR), non-preemptive and preemptive Priority Scheduling (with optional aging), Highest Response Ratio Next (HRRN), and Multilevel Queue scheduling.
|
||||||
|
|
||||||
@@ -39,13 +39,13 @@ PSS addresses this gap by providing a zero-installation, zero-configuration simu
|
|||||||
- **Instructors** seeking a live, configurable demonstration tool for classroom or remote teaching;
|
- **Instructors** seeking a live, configurable demonstration tool for classroom or remote teaching;
|
||||||
- **Researchers and practitioners** who need a quick, reproducible environment to illustrate scheduling concepts.
|
- **Researchers and practitioners** who need a quick, reproducible environment to illustrate scheduling concepts.
|
||||||
|
|
||||||
The configurable parameters — quantum for Round Robin, aging interval and threshold for Priority Scheduling, and per-queue quantum for Multilevel — enable learners to observe how parameter choices affect fairness, throughput, and starvation, all within a single interactive session.
|
The configurable parameters (such as quantum for Round Robin, aging interval and threshold for Priority Scheduling, and per-queue quantum for Multilevel) enable learners to observe how parameter choices affect fairness, throughput, and starvation, all within a single interactive session.
|
||||||
|
|
||||||
# State of the Field
|
# State of the Field
|
||||||
|
|
||||||
Several browser-based and desktop scheduling simulators exist. OS-sim [@ossim] offers a graphical simulation of a full operating system, including scheduling, but targets upper-division students and requires local installation. The CPU Scheduling Simulator by Mitchell [@mitchelljava] is a Java desktop application that covers a subset of algorithms without I/O burst modeling. Web-based tools such as Process Scheduling Simulator by Nikhil Anand cover FCFS, SJF, and RR, but do not support aging, HRRN, or Multilevel Queue, and lack composable I/O burst sequences.
|
Several browser-based and desktop scheduling simulators exist. OS-sim [@ossim] offers a graphical simulation of a full operating system, including scheduling, but targets upper-division students and requires local installation. The CPU Scheduling Simulator by Mitchell [@mitchelljava] is a Java desktop application that covers a subset of algorithms without I/O burst modeling. Web-based tools such as Process Scheduling Simulator by Nikhil Anand cover FCFS, SJF, and RR, but do not support aging, HRRN, or Multilevel Queue, and lack composable I/O burst sequences.
|
||||||
|
|
||||||
PSS distinguishes itself in three key ways. First, it models **full CPU-I/O burst sequences** for each process, rather than a single CPU burst, enabling simulation of realistic workloads and the CPU Bound vs. I/O Bound classification. Second, it implements a broader set of algorithms — including HRRN and Multilevel Queue with per-queue quantum — than most comparable tools. Third, the entire application is deployed as a **static single-page application** with no backend dependency, enabling one-click access and offline use via Docker, which lowers the barrier for classroom adoption.
|
PSS distinguishes itself in three key ways. First, it models **full CPU-I/O burst sequences** for each process, rather than a single CPU burst, enabling simulation of realistic workloads and the CPU Bound vs. I/O Bound classification. Second, it implements a broader set of algorithms than most comparable tools, including HRRN and Multilevel Queue with per-queue quantum. Third, the entire application is deployed as a **static single-page application** with no backend dependency, enabling one-click access and offline use via Docker, which lowers the barrier for classroom adoption.
|
||||||
|
|
||||||
# Software Design
|
# Software Design
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ PSS was developed as part of the Computer Science undergraduate program at the U
|
|||||||
|
|
||||||
The availability of PSS as a fully open-source project under the MIT license, with Docker and `npm run dev` deployment paths, enables any institution to adopt or adapt the simulator without cost or proprietary dependency. The live deployment at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br) provides a stable public URL suitable for sharing in syllabi, assignments, and online course materials.
|
The availability of PSS as a fully open-source project under the MIT license, with Docker and `npm run dev` deployment paths, enables any institution to adopt or adapt the simulator without cost or proprietary dependency. The live deployment at [https://pss.lucasrguerra.dev.br](https://pss.lucasrguerra.dev.br) provides a stable public URL suitable for sharing in syllabi, assignments, and online course materials.
|
||||||
|
|
||||||
The built-in preset scenarios — including `convoy_effect`, `starvation`, and `aging_fix` — provide reproducible, named examples of classical OS phenomena that instructors can reference directly in lectures, assignments, or exam questions, facilitating standardized discussion of algorithm trade-offs.
|
The built-in preset scenarios, which include `convoy_effect`, `starvation`, and `aging_fix`, provide reproducible, named examples of classical OS phenomena that instructors can reference directly in lectures, assignments, or exam questions, facilitating standardized discussion of algorithm trade-offs.
|
||||||
|
|
||||||
# AI Usage Disclosure
|
# AI Usage Disclosure
|
||||||
|
|
||||||
@@ -73,6 +73,6 @@ The author used AI-assisted coding tools (GitHub Copilot and Google Gemini) duri
|
|||||||
|
|
||||||
# Acknowledgements
|
# Acknowledgements
|
||||||
|
|
||||||
The author thanks the professors and students of the Operating Systems course at UFRPE whose feedback shaped the features and usability of PSS.
|
This software was developed to address specific requirements identified during a personal case study. The author acknowledges the professors and students of the Operating Systems course at UFRPE for their valuable comments and feedback provided after the tool was presented.
|
||||||
|
|
||||||
# References
|
# References
|
||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
author = {Robbins, Kay A.},
|
author = {Robbins, Kay A.},
|
||||||
title = {{OS-sim}: A Graphical, Interprocess Operating System Simulator},
|
title = {{OS-sim}: A Graphical, Interprocess Operating System Simulator},
|
||||||
year = {2012},
|
year = {2012},
|
||||||
howpublished = {\url{https://github.com/mayank-02/multithreading-library}},
|
howpublished = {\url{https://sourceforge.net/projects/oscsimulator/}},
|
||||||
note = {Used in OS coursework at several universities}
|
note = {Used in OS coursework at several universities}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Uso: scripts/plano-imagem.sh <registro> <repositório>
|
||||||
|
# Imprime inputs=, image= e reuse= (para $GITHUB_OUTPUT). O hash cobre só os
|
||||||
|
# arquivos que entram no build da imagem, não a documentação nem a configuração
|
||||||
|
# de ferramentas. reuse=true quando o registro já tem inputs-<hash>.
|
||||||
|
set -euo pipefail
|
||||||
|
registry=$1; ns=${2,,}
|
||||||
|
|
||||||
|
inputs=$(git ls-tree -r HEAD \
|
||||||
|
| awk -F'\t' '$2 !~ /^(\.gitea|\.github|\.claude|\.wolf|\.superset|docs)\// && $2 !~ /^(README.*|CONTRIBUTING\.md|CHANGELOG\.md|CITATION\.cff|LICENSE|paper\.md|references\.bib|\.gitignore|\.gitattributes|scripts\/release-notes\.sh)$/' \
|
||||||
|
| sha256sum | cut -c1-16)
|
||||||
|
token=$(curl -fsS "https://$registry/v2/token?service=container_registry&scope=repository:$ns:pull" | jq -r .token)
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' -I \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json' \
|
||||||
|
"https://$registry/v2/$ns/manifests/inputs-$inputs")
|
||||||
|
echo "inputs-$inputs -> HTTP $code" >&2
|
||||||
|
reuse=false; [ "$code" = 200 ] && reuse=true
|
||||||
|
echo "inputs=$inputs"
|
||||||
|
echo "image=$registry/$ns"
|
||||||
|
echo "reuse=$reuse"
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Imprime as notas de uma release: a imagem publicada (quando houver) e a seção
|
||||||
|
# do CHANGELOG.md daquela versão (ou o log de commits desde a tag anterior, se
|
||||||
|
# não houver seção).
|
||||||
|
#
|
||||||
|
# scripts/release-notes.sh v1.2.3 [git.exemplo.com/dono/projeto]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
tag=${1:?uso: release-notes.sh <tag> [imagem]}
|
||||||
|
image=${2:-}
|
||||||
|
version=${tag#v}
|
||||||
|
|
||||||
|
if [ -n "$image" ]; then
|
||||||
|
echo "## Imagem"
|
||||||
|
echo
|
||||||
|
echo '```'
|
||||||
|
echo "$image:$version"
|
||||||
|
echo '```'
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
section=""
|
||||||
|
if [ -f CHANGELOG.md ]; then
|
||||||
|
section=$(awk -v head="## [$version]" '
|
||||||
|
index($0, head) == 1 { found = 1; next }
|
||||||
|
found && /^## \[/ { exit }
|
||||||
|
found && /^\[[^]]+\]: / { next }
|
||||||
|
found { print }
|
||||||
|
' CHANGELOG.md)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
|
||||||
|
printf '%s\n' "$section"
|
||||||
|
else
|
||||||
|
previous=$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)
|
||||||
|
echo "## Mudanças${previous:+ desde $previous}"
|
||||||
|
echo
|
||||||
|
git log --no-merges --format='- %s (%h)' "${previous:+$previous..}$tag"
|
||||||
|
fi
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Teste de fumaça da imagem: a SPA serve o index, cai no index em rotas internas
|
||||||
|
# e entrega os assets com cache longo.
|
||||||
|
#
|
||||||
|
# scripts/smoke-imagem.sh http://127.0.0.1:3998
|
||||||
|
#
|
||||||
|
# Cada falha é impressa e o script sai com 1 no fim, para mostrar tudo de uma vez.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
base=${1:?uso: smoke-imagem.sh <url-base>}
|
||||||
|
falhas=0
|
||||||
|
falhou() { echo "FALHOU $1"; falhas=$((falhas + 1)); }
|
||||||
|
|
||||||
|
# O nginx leva um instante para subir.
|
||||||
|
for _ in $(seq 1 30); do curl -fsS -o /dev/null "$base/" 2>/dev/null && break; sleep 1; done
|
||||||
|
|
||||||
|
index=$(curl -fsS "$base/") || { echo "FALHOU /: sem resposta"; exit 1; }
|
||||||
|
grep -q 'id="root"' <<< "$index" || falhou "/: o index não tem o #root"
|
||||||
|
|
||||||
|
# Rota interna da SPA devolve o mesmo index (try_files), não 404.
|
||||||
|
[ "$(curl -s -o /dev/null -w '%{http_code}' "$base/qualquer/rota")" = 200 ] || falhou "/qualquer/rota: esperado 200"
|
||||||
|
|
||||||
|
# Todo asset que o index referencia existe e tem cache imutável.
|
||||||
|
assets=$(grep -oE '(src|href)="/assets/[^"]+"' <<< "$index" | cut -d'"' -f2)
|
||||||
|
[ -n "$assets" ] || falhou "o index não referencia nenhum /assets/"
|
||||||
|
for a in $assets; do
|
||||||
|
head=$(curl -sI "$base$a")
|
||||||
|
grep -q '^HTTP/[0-9.]* 200' <<< "$head" || falhou "$a: esperado 200"
|
||||||
|
grep -qi 'cache-control:.*immutable' <<< "$head" || falhou "$a: sem cache imutável"
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "$falhas" = 0 ] && echo "ok: $(wc -w <<< "$assets") assets e rotas conferidos"
|
||||||
|
[ "$falhas" = 0 ]
|
||||||
@@ -15,10 +15,10 @@ const SPEED_LABELS: Record<number, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const statusLabel: Record<SimStatus, string> = {
|
const statusLabel: Record<SimStatus, string> = {
|
||||||
idle: 'Pronto',
|
idle: 'Ready',
|
||||||
paused: 'Pausado',
|
paused: 'Paused',
|
||||||
running: 'Executando',
|
running: 'Running',
|
||||||
finished: 'Concluído',
|
finished: 'Finished',
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusDotClass: Record<SimStatus, string> = {
|
const statusDotClass: Record<SimStatus, string> = {
|
||||||
@@ -66,9 +66,9 @@ const ControlBar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
className="h-16 py-2 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label="Controles da simulação"
|
aria-label="Simulation controls"
|
||||||
>
|
>
|
||||||
{/* Left group: simulation controls */}
|
{/* Left group: simulation controls */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -85,7 +85,7 @@ const ControlBar = () => {
|
|||||||
|
|
||||||
{/* Step */}
|
{/* Step */}
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Avançar 1 tick (Step)"
|
label="Advance 1 tick (Step)"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleStep}
|
onClick={handleStep}
|
||||||
disabled={isFinished || processes.length === 0 || isRunning}
|
disabled={isFinished || processes.length === 0 || isRunning}
|
||||||
@@ -97,7 +97,7 @@ const ControlBar = () => {
|
|||||||
{/* Play / Pause */}
|
{/* Play / Pause */}
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Pausar simulação"
|
label="Pause simulation"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={pause}
|
onClick={pause}
|
||||||
data-testid="btn-pause"
|
data-testid="btn-pause"
|
||||||
@@ -106,7 +106,7 @@ const ControlBar = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
) : (
|
) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Iniciar / Retomar simulação"
|
label="Start / Resume simulation"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
disabled={!canPlay && !isRunning}
|
disabled={!canPlay && !isRunning}
|
||||||
@@ -124,7 +124,7 @@ const ControlBar = () => {
|
|||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700"
|
className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label={`Tick atual: ${currentTick}`}
|
aria-label={`Current tick: ${currentTick}`}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span>
|
<span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span>
|
||||||
<span
|
<span
|
||||||
@@ -145,14 +145,14 @@ const ControlBar = () => {
|
|||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<SkipBack size={14} className="text-slate-500" aria-hidden="true" />
|
<SkipBack size={14} className="text-slate-500" aria-hidden="true" />
|
||||||
<label htmlFor="speed-select" className="text-xs text-slate-400 sr-only">
|
<label htmlFor="speed-select" className="text-xs text-slate-400 sr-only">
|
||||||
Velocidade
|
Speed
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="speed-select"
|
id="speed-select"
|
||||||
value={speed}
|
value={speed}
|
||||||
onChange={e => setSpeed(Number(e.target.value))}
|
onChange={e => setSpeed(Number(e.target.value))}
|
||||||
className="rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
className="rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||||
aria-label="Velocidade de simulação"
|
aria-label="Simulation speed"
|
||||||
data-testid="speed-select"
|
data-testid="speed-select"
|
||||||
>
|
>
|
||||||
{SPEED_OPTIONS.map(s => (
|
{SPEED_OPTIONS.map(s => (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useSimulationStore } from "@store/simulationStore";
|
|||||||
import { useProcessStore } from "@store/processStore";
|
import { useProcessStore } from "@store/processStore";
|
||||||
import { useGanttScroll } from "@hooks/useGanttScroll";
|
import { useGanttScroll } from "@hooks/useGanttScroll";
|
||||||
import GanttRow from "./GanttRow";
|
import GanttRow from "./GanttRow";
|
||||||
|
import GanttThreadRow from "./GanttThreadRow";
|
||||||
import GanttLegend from "./GanttLegend";
|
import GanttLegend from "./GanttLegend";
|
||||||
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
||||||
import { CELL_H, CELL_W } from "./constants";
|
import { CELL_H, CELL_W } from "./constants";
|
||||||
@@ -13,33 +14,29 @@ const GanttChart = () => {
|
|||||||
const status = useSimulationStore((s) => s.status);
|
const status = useSimulationStore((s) => s.status);
|
||||||
const processes = useProcessStore((s) => s.processes);
|
const processes = useProcessStore((s) => s.processes);
|
||||||
|
|
||||||
// Scrollable container ref for auto-scroll
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const currentTickIndex = ticks.length - 1;
|
const currentTickIndex = ticks.length - 1;
|
||||||
useGanttScroll(scrollRef, currentTickIndex, status === "running");
|
useGanttScroll(scrollRef, currentTickIndex, status === "running");
|
||||||
|
|
||||||
// Tooltip state
|
|
||||||
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
|
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
|
||||||
const handleMouseLeave = useCallback(() => setTooltip(null), []);
|
const handleMouseLeave = useCallback(() => setTooltip(null), []);
|
||||||
|
|
||||||
// Empty state
|
|
||||||
if (ticks.length === 0) {
|
if (ticks.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
|
<div
|
||||||
style={{ minHeight: 80 }}>
|
className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
|
||||||
Inicie a simulação para ver o diagrama de Gantt
|
style={{ minHeight: 80 }}
|
||||||
|
>
|
||||||
|
Start the simulation to see the Gantt chart
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ruler (tick numbers) ──────────────────────────────────
|
|
||||||
const tickNumbers = ticks.map((t) => t.tick);
|
const tickNumbers = ticks.map((t) => t.tick);
|
||||||
const totalWidth = ticks.length * CELL_W;
|
const totalWidth = ticks.length * CELL_W;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
|
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
|
||||||
|
|
||||||
{/* ── scrollable grid + cursor ── */}
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
|
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
|
||||||
@@ -48,9 +45,10 @@ const GanttChart = () => {
|
|||||||
|
|
||||||
{/* Tick ruler */}
|
{/* Tick ruler */}
|
||||||
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
||||||
{/* Spacer for sticky process labels column */}
|
<div
|
||||||
<div className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0" style={{ width: 48, height: 16 }} />
|
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
|
||||||
|
style={{ width: 60, height: 16 }}
|
||||||
|
/>
|
||||||
{tickNumbers.map((t) => (
|
{tickNumbers.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t}
|
key={t}
|
||||||
@@ -62,16 +60,25 @@ const GanttChart = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process rows */}
|
{/* Process rows (+ thread sub-rows) */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{processes.map((process) => (
|
{processes.map((process) => {
|
||||||
<div key={process.id} className="flex items-center border-b border-white/5 last:border-b-0">
|
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||||
{/* Process label (sticky on the left) */}
|
|
||||||
|
return (
|
||||||
|
<div key={process.id}>
|
||||||
|
{/* ── Linha do processo ─────────────────────────── */}
|
||||||
|
<div
|
||||||
|
className={`flex items-center border-b border-white/5 ${hasThreads ? 'border-b-0' : ''}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
|
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
|
||||||
style={{ width: 48, height: CELL_H }}
|
style={{ width: 60, height: CELL_H }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-bold truncate"
|
||||||
|
style={{ color: process.color }}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] font-bold truncate" style={{ color: process.color }}>
|
|
||||||
{process.name}
|
{process.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,13 +90,34 @@ const GanttChart = () => {
|
|||||||
onCellMouseLeave={handleMouseLeave}
|
onCellMouseLeave={handleMouseLeave}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Sub-linhas de threads ─────────────────────── */}
|
||||||
|
{hasThreads &&
|
||||||
|
process.threads!.map((thread, threadIdx) => (
|
||||||
|
<GanttThreadRow
|
||||||
|
key={thread.tid}
|
||||||
|
thread={thread}
|
||||||
|
threadIndex={threadIdx}
|
||||||
|
process={process}
|
||||||
|
ticks={ticks}
|
||||||
|
onCellMouseEnter={setTooltip}
|
||||||
|
onCellMouseLeave={handleMouseLeave}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Separador após o bloco de threads */}
|
||||||
|
{hasThreads && (
|
||||||
|
<div className="border-b border-white/5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Current-tick cursor line */}
|
{/* Current-tick cursor line */}
|
||||||
{currentTickIndex >= 0 && (
|
{currentTickIndex >= 0 && (
|
||||||
<div
|
<div
|
||||||
className="gantt-cursor absolute top-0 bottom-0 w-px bg-white/20 pointer-events-none"
|
className="gantt-cursor absolute top-0 bottom-0 w-px bg-white/20 pointer-events-none"
|
||||||
style={{ left: 48 + currentTickIndex * CELL_W + CELL_W / 2 }}
|
style={{ left: 60 + currentTickIndex * CELL_W + CELL_W / 2 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -97,14 +125,7 @@ const GanttChart = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── process name labels (sticky left, overlay) ── */}
|
|
||||||
{/* Rendered OUTSIDE the scrollable div so they stay fixed on the left */}
|
|
||||||
{/* We achieve this with a separate absolute-positioned column */}
|
|
||||||
|
|
||||||
{/* ── legend ── */}
|
|
||||||
<GanttLegend />
|
<GanttLegend />
|
||||||
|
|
||||||
{/* ── tooltip ── */}
|
|
||||||
{tooltip && <GanttTooltip data={tooltip} />}
|
{tooltip && <GanttTooltip data={tooltip} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Process, SimTick, StateLabel, Thread } from "@core/types";
|
||||||
|
import { CELL_W } from "./constants";
|
||||||
|
import type { TooltipData } from "./GanttTooltip";
|
||||||
|
import { deriveThreadColor } from "../shared/threadUtils";
|
||||||
|
|
||||||
|
const THREAD_CELL_H = 20;
|
||||||
|
|
||||||
|
// ── cores de estado (mesmas do processo) ────────────────────
|
||||||
|
const STATE_CLASS: Record<StateLabel, string> = {
|
||||||
|
New: "bg-[var(--color-state-new)]",
|
||||||
|
Ready: "bg-[var(--color-state-ready)]",
|
||||||
|
Running: "bg-[var(--color-state-running)]",
|
||||||
|
Waiting: "bg-[var(--color-state-waiting)]",
|
||||||
|
Terminated: "bg-[var(--color-state-terminated)]",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GanttThreadRowProps {
|
||||||
|
thread: Thread;
|
||||||
|
threadIndex: number;
|
||||||
|
process: Process;
|
||||||
|
ticks: SimTick[];
|
||||||
|
onCellMouseEnter: (data: TooltipData) => void;
|
||||||
|
onCellMouseLeave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GanttThreadRow = React.memo(function GanttThreadRow({
|
||||||
|
thread,
|
||||||
|
threadIndex,
|
||||||
|
process,
|
||||||
|
ticks,
|
||||||
|
onCellMouseEnter,
|
||||||
|
onCellMouseLeave,
|
||||||
|
}: GanttThreadRowProps) {
|
||||||
|
const threadColor = deriveThreadColor(process.color, threadIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center border-b border-white/3">
|
||||||
|
{/* Rótulo da thread (sticky) */}
|
||||||
|
<div
|
||||||
|
className="sticky left-0 z-20 bg-slate-900/90 border-r border-slate-700/50 px-2 flex items-center shrink-0 gap-1"
|
||||||
|
style={{ width: 60, height: THREAD_CELL_H }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: threadColor }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="text-[8px] font-medium truncate"
|
||||||
|
style={{ color: threadColor }}
|
||||||
|
>
|
||||||
|
{thread.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Células da thread */}
|
||||||
|
{ticks.map((tick) => {
|
||||||
|
const threadState: StateLabel =
|
||||||
|
tick.threadStates?.[process.id]?.[thread.tid] ?? "New";
|
||||||
|
|
||||||
|
const isActive = tick.cpuThreads?.[process.id] === thread.tid;
|
||||||
|
const isKernelWait =
|
||||||
|
tick.kernelWaitingThreads?.[process.id]?.includes(thread.tid) ?? false;
|
||||||
|
|
||||||
|
const bg = STATE_CLASS[threadState];
|
||||||
|
|
||||||
|
// Borda colorida para thread ativa (Running)
|
||||||
|
const borderStyle =
|
||||||
|
isActive && threadState === "Running"
|
||||||
|
? { borderLeft: `2px solid ${threadColor}` }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Padrão diagonal para kernel wait (MANY_TO_MANY)
|
||||||
|
const extraClass = isKernelWait
|
||||||
|
? "opacity-50"
|
||||||
|
: "opacity-80 hover:opacity-100";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tick.tick}
|
||||||
|
className={`${bg} ${extraClass} shrink-0 transition-opacity cursor-default`}
|
||||||
|
style={{ width: CELL_W, height: THREAD_CELL_H, ...borderStyle }}
|
||||||
|
title={`${thread.name} — tick ${tick.tick}: ${threadState}${isKernelWait ? " (KernelWait)" : ""}`}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
onCellMouseEnter({
|
||||||
|
tick,
|
||||||
|
process,
|
||||||
|
state: threadState,
|
||||||
|
isCtxSwitch: false,
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMouseLeave={onCellMouseLeave}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{ticks.length === 0 && (
|
||||||
|
<div style={{ width: CELL_W, height: THREAD_CELL_H }} className="bg-slate-800/40" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GanttThreadRow;
|
||||||
@@ -66,7 +66,7 @@ const AppHeader = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setPresetsOpen(o => !o)}
|
onClick={() => setPresetsOpen(o => !o)}
|
||||||
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
|
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
|
||||||
aria-label="Carregar cenário pré-definido"
|
aria-label="Load preset scenario"
|
||||||
>
|
>
|
||||||
<BookOpen size={13} />
|
<BookOpen size={13} />
|
||||||
Presets
|
Presets
|
||||||
@@ -83,10 +83,18 @@ const AppHeader = () => {
|
|||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden flex flex-col max-h-[min(32rem,calc(100vh-4rem))]">
|
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden flex flex-col max-h-[min(32rem,calc(100vh-4rem))]">
|
||||||
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium shrink-0">
|
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium shrink-0">
|
||||||
Cenários pré-definidos
|
Preset Scenarios
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto">
|
<div className="overflow-y-auto">
|
||||||
{presets.map((preset, i) => (
|
{/* Classic process presets */}
|
||||||
|
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
|
||||||
|
Processes
|
||||||
|
</div>
|
||||||
|
{presets
|
||||||
|
.filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||||
|
.map((preset) => {
|
||||||
|
const i = presets.indexOf(preset);
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={preset.name}
|
key={preset.name}
|
||||||
onClick={() => handleLoadPreset(i)}
|
onClick={() => handleLoadPreset(i)}
|
||||||
@@ -102,7 +110,37 @@ const AppHeader = () => {
|
|||||||
{preset.config.algorithm}
|
{preset.config.algorithm}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Thread presets */}
|
||||||
|
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0 border-t border-slate-700 mt-1">
|
||||||
|
Threads
|
||||||
|
</div>
|
||||||
|
{presets
|
||||||
|
.filter(p => ['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||||
|
.map((preset) => {
|
||||||
|
const i = presets.indexOf(preset);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={preset.name}
|
||||||
|
onClick={() => handleLoadPreset(i)}
|
||||||
|
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="text-xs font-medium text-slate-200 group-hover:text-white">
|
||||||
|
{preset.name}
|
||||||
|
</div>
|
||||||
|
<span className="text-[8px] px-1 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
|
||||||
|
THREADS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
|
||||||
|
{preset.description}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -112,16 +150,16 @@ const AppHeader = () => {
|
|||||||
{/* Import JSON */}
|
{/* Import JSON */}
|
||||||
<label
|
<label
|
||||||
htmlFor="import-json"
|
htmlFor="import-json"
|
||||||
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'}
|
title={isSimulationActive ? 'Reset the simulation before importing' : 'Import JSON configuration'}
|
||||||
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
|
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
|
||||||
isSimulationActive
|
isSimulationActive
|
||||||
? 'text-slate-600 cursor-not-allowed'
|
? 'text-slate-600 cursor-not-allowed'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-700'
|
: 'text-slate-300 hover:text-white hover:bg-slate-700'
|
||||||
}`}
|
}`}
|
||||||
aria-label="Importar configuração JSON"
|
aria-label="Import JSON configuration"
|
||||||
>
|
>
|
||||||
<Upload size={13} />
|
<Upload size={13} />
|
||||||
Importar
|
Import
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="import-json"
|
id="import-json"
|
||||||
@@ -131,19 +169,19 @@ const AppHeader = () => {
|
|||||||
className="sr-only"
|
className="sr-only"
|
||||||
onChange={handleImportJSON}
|
onChange={handleImportJSON}
|
||||||
disabled={isSimulationActive}
|
disabled={isSimulationActive}
|
||||||
aria-label="Selecionar arquivo JSON para importar"
|
aria-label="Select JSON file to import"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Export JSON */}
|
{/* Export JSON */}
|
||||||
<button
|
<button
|
||||||
onClick={handleExportJSON}
|
onClick={handleExportJSON}
|
||||||
disabled={processes.length === 0}
|
disabled={processes.length === 0}
|
||||||
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'}
|
title={processes.length === 0 ? 'Add processes before exporting' : 'Export configuration as JSON'}
|
||||||
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
aria-label="Exportar configuração JSON"
|
aria-label="Export JSON configuration"
|
||||||
>
|
>
|
||||||
<Download size={13} />
|
<Download size={13} />
|
||||||
Exportar
|
Export
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import MetricsPanel from '../MetricsPanel/MetricsPanel';
|
|||||||
|
|
||||||
const MainArea = () => {
|
const MainArea = () => {
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
|
<main className="flex-1 flex flex-col bg-slate-900 overflow-y-auto relative">
|
||||||
<ControlBar />
|
<ControlBar />
|
||||||
<GanttChart />
|
<GanttChart />
|
||||||
<MetricsPanel />
|
<MetricsPanel />
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
|
|||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
return (
|
return (
|
||||||
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
|
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
|
||||||
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
|
|
||||||
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
|
|
||||||
Process Simulator
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 scrollbar-hide">
|
||||||
<ProcessPanel />
|
<ProcessPanel />
|
||||||
<div className="border-t border-slate-700 pt-4">
|
<div className="border-t border-slate-700 pt-4">
|
||||||
@@ -18,7 +12,7 @@ const Sidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
|
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
|
||||||
PSS v1.0
|
PSS v1.3.3
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
label="Avg Response Time"
|
label="Avg Response Time"
|
||||||
value={avgResponseTime.toFixed(2)}
|
value={avgResponseTime.toFixed(2)}
|
||||||
sub="unidades de tempo"
|
sub="time units"
|
||||||
accentColor="#f59e0b"
|
accentColor="#f59e0b"
|
||||||
accentClass="text-amber-300"
|
accentClass="text-amber-300"
|
||||||
icon={<Clock size={13} />}
|
icon={<Clock size={13} />}
|
||||||
@@ -79,7 +79,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
label="Avg Turnaround Time"
|
label="Avg Turnaround Time"
|
||||||
value={avgTurnaroundTime.toFixed(2)}
|
value={avgTurnaroundTime.toFixed(2)}
|
||||||
sub="unidades de tempo"
|
sub="time units"
|
||||||
accentColor="#60a5fa"
|
accentColor="#60a5fa"
|
||||||
accentClass="text-blue-300"
|
accentClass="text-blue-300"
|
||||||
icon={<Timer size={13} />}
|
icon={<Timer size={13} />}
|
||||||
@@ -88,7 +88,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
label="Avg Waiting Time"
|
label="Avg Waiting Time"
|
||||||
value={avgWaitingTime.toFixed(2)}
|
value={avgWaitingTime.toFixed(2)}
|
||||||
sub="unidades de tempo"
|
sub="time units"
|
||||||
accentColor="#fb923c"
|
accentColor="#fb923c"
|
||||||
accentClass="text-orange-300"
|
accentClass="text-orange-300"
|
||||||
icon={<Hourglass size={13} />}
|
icon={<Hourglass size={13} />}
|
||||||
@@ -97,7 +97,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
label="CPU Throughput"
|
label="CPU Throughput"
|
||||||
value={cpuThroughput.toFixed(3)}
|
value={cpuThroughput.toFixed(3)}
|
||||||
sub="processos / u.t."
|
sub="processes / t.u."
|
||||||
accentColor="#34d399"
|
accentColor="#34d399"
|
||||||
accentClass="text-emerald-300"
|
accentClass="text-emerald-300"
|
||||||
icon={<Zap size={13} />}
|
icon={<Zap size={13} />}
|
||||||
@@ -120,7 +120,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
|||||||
<span className="text-2xl font-bold font-mono leading-none text-violet-300">
|
<span className="text-2xl font-bold font-mono leading-none text-violet-300">
|
||||||
{utilizationPct.toFixed(1)}%
|
{utilizationPct.toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-slate-500">do tempo total em CPU</span>
|
<span className="text-[10px] text-slate-500">of total CPU time</span>
|
||||||
<div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden">
|
<div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-500"
|
className="h-full rounded-full transition-all duration-500"
|
||||||
|
|||||||
@@ -28,11 +28,27 @@ const TOOLTIP_STYLE = {
|
|||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper that gives ResponsiveContainer a bounded, non-growing parent.
|
||||||
|
*
|
||||||
|
* Without this, recharts' ResizeObserver inside a scroll container can trigger
|
||||||
|
* a resize loop: scrollbar appears → available width shrinks → chart redraws →
|
||||||
|
* height shifts → scrollbar disappears → repeat. The explicit height + overflow
|
||||||
|
* hidden caps the chart's DOM footprint and breaks the loop.
|
||||||
|
*/
|
||||||
|
const ChartBox = ({ height, children }: { height: number; children: React.ReactNode }) => (
|
||||||
|
<div style={{ width: '100%', height, overflow: 'hidden', flexShrink: 0 }}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
{children as React.ReactElement}
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||||
if (metrics.length === 0) {
|
if (metrics.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
||||||
Nenhum dado disponível.
|
No data available.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,6 +56,35 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
const getName = (id: string) => processMap[id]?.name ?? id;
|
const getName = (id: string) => processMap[id]?.name ?? id;
|
||||||
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
|
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
|
||||||
|
|
||||||
|
// Reverse map: process name → process (for radar tooltip lookup)
|
||||||
|
const nameToProcess = Object.fromEntries(
|
||||||
|
Object.values(processMap).map(p => [p.name, p])
|
||||||
|
);
|
||||||
|
|
||||||
|
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
|
||||||
|
if (!active || !payload?.length) return null;
|
||||||
|
return (
|
||||||
|
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
|
||||||
|
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{payload.map((entry: any) => {
|
||||||
|
const proc = nameToProcess[entry.name];
|
||||||
|
const displayName = proc?.pid != null ? `#${proc.pid} ${proc.name}` : entry.name;
|
||||||
|
return (
|
||||||
|
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
|
||||||
|
<span style={{ color: '#cbd5e1' }}>{displayName}</span>
|
||||||
|
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
|
||||||
|
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ── Stacked bar data ──────────────────────────────────────────
|
// ── Stacked bar data ──────────────────────────────────────────
|
||||||
const barData = metrics.map((m) => ({
|
const barData = metrics.map((m) => ({
|
||||||
name: getName(m.processId),
|
name: getName(m.processId),
|
||||||
@@ -51,7 +96,6 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
|
|
||||||
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||||
// Each "subject" is a process, each series is a dimension.
|
// Each "subject" is a process, each series is a dimension.
|
||||||
// We invert "Response" and "Waiting" so higher = better.
|
|
||||||
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||||
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||||
@@ -72,7 +116,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||||
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||||
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||||
case 'CPU Time': row[key] = (m.cpuTime / maxCpu)* 100; break;
|
case 'CPU Time': row[key] = (m.cpuTime / maxCpu) * 100; break;
|
||||||
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -81,15 +125,18 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
|
|
||||||
const showRadar = metrics.length >= 2;
|
const showRadar = metrics.length >= 2;
|
||||||
|
|
||||||
|
// Charts are always stacked vertically (flex-col) so that the total height
|
||||||
|
// exceeds the panel's viewport, making the scroll area in MetricsPanel
|
||||||
|
// active and accessible on any screen size.
|
||||||
return (
|
return (
|
||||||
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
|
<div className="p-3 flex flex-col gap-6">
|
||||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||||
{showRadar && (
|
{showRadar && (
|
||||||
<div className="flex-1 min-w-0">
|
<div className="w-full">
|
||||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||||
Perfil Comparativo (normalizado)
|
Comparative Profile (normalized)
|
||||||
</p>
|
</p>
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
<ChartBox height={360}>
|
||||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||||
<PolarGrid stroke="#334155" />
|
<PolarGrid stroke="#334155" />
|
||||||
<PolarAngleAxis
|
<PolarAngleAxis
|
||||||
@@ -108,24 +155,21 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
dot={{ r: 3, fill: getColor(m.processId) }}
|
dot={{ r: 3, fill: getColor(m.processId) }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Tooltip
|
<Tooltip content={radarTooltipContent} />
|
||||||
contentStyle={TOOLTIP_STYLE}
|
|
||||||
formatter={(value) => [typeof value === 'number' ? `${value.toFixed(1)}%` : '-', '']}
|
|
||||||
/>
|
|
||||||
<Legend
|
<Legend
|
||||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||||
/>
|
/>
|
||||||
</RadarChart>
|
</RadarChart>
|
||||||
</ResponsiveContainer>
|
</ChartBox>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||||
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
|
<div className="w-full">
|
||||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||||
Breakdown por Processo (ticks)
|
Breakdown per Process (ticks)
|
||||||
</p>
|
</p>
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
<ChartBox height={360}>
|
||||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
@@ -157,7 +201,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
|||||||
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||||
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ChartBox>
|
||||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||||
Waiting + CPU + I/O = Turnaround
|
Waiting + CPU + I/O = Turnaround
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Download, BarChart2, Table, Globe } from 'lucide-react';
|
import { Download, BarChart2, Table, Globe, Layers } from 'lucide-react';
|
||||||
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
|
import { computeProcessMetrics, computeGlobalMetrics, computeThreadMetrics } from '@core/metrics';
|
||||||
import { useSimulationStore } from '../../store/simulationStore';
|
import { useSimulationStore } from '../../store/simulationStore';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
import { useProcessStore } from '../../store/processStore';
|
||||||
import { useExport } from '../../hooks/useExport';
|
import { useExport } from '../../hooks/useExport';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import GlobalMetrics from './GlobalMetrics';
|
import GlobalMetrics from './GlobalMetrics';
|
||||||
import MetricsChart from './MetricsChart';
|
import MetricsChart from './MetricsChart';
|
||||||
|
import ThreadMetricsTable from './ThreadMetricsTable';
|
||||||
|
import ThreadMetricsChart from './ThreadMetricsChart';
|
||||||
|
|
||||||
type Tab = 'table' | 'global' | 'chart';
|
type Tab = 'table' | 'global' | 'charts' | 'threads' | 'thread-charts';
|
||||||
|
|
||||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
|
||||||
{ id: 'table', label: 'Por Processo', icon: <Table size={13} /> },
|
|
||||||
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
|
|
||||||
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MetricsPanel = () => {
|
const MetricsPanel = () => {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('table');
|
const [activeTab, setActiveTab] = useState<Tab>('table');
|
||||||
@@ -31,25 +27,45 @@ const MetricsPanel = () => {
|
|||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
|
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
|
||||||
Execute a simulação para ver as métricas.
|
Run the simulation to see metrics.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive metrics from engine runtime state
|
// ── Métricas de processos ──────────────────────────────────────────────
|
||||||
const runtimes = engine?.runtimeStates ?? [];
|
const runtimes = engine?.runtimeStates ?? [];
|
||||||
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
|
const completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
|
||||||
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
|
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
|
||||||
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
|
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
|
||||||
const processMap = Object.fromEntries(processes.map(p => [p.id, p]));
|
const processMap = Object.fromEntries(processes.map(p => [p.id, p]));
|
||||||
|
|
||||||
|
// ── Métricas de threads ────────────────────────────────────────────────
|
||||||
|
const threadRuntimes = engine?.threadRuntimeStates ?? [];
|
||||||
|
const completedThreadRts = threadRuntimes.filter(trt => trt.finishTick !== null);
|
||||||
|
const threadMetrics = completedThreadRts.map(trt => {
|
||||||
|
const process = processMap[trt.processId];
|
||||||
|
const thread = process?.threads?.find(t => t.tid === trt.threadId);
|
||||||
|
if (!process || !thread) return null;
|
||||||
|
return computeThreadMetrics(trt, thread, process);
|
||||||
|
}).filter(Boolean) as ReturnType<typeof computeThreadMetrics>[];
|
||||||
|
|
||||||
|
const hasThreads = processes.some(p => (p.threads?.length ?? 0) > 0);
|
||||||
|
|
||||||
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
||||||
|
|
||||||
|
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode; show: boolean }[] = [
|
||||||
|
{ id: 'table', label: 'Per Process', icon: <Table size={13} />, show: true },
|
||||||
|
{ id: 'global', label: 'Global', icon: <Globe size={13} />, show: true },
|
||||||
|
{ id: 'charts', label: 'Process Charts', icon: <BarChart2 size={13} />, show: true },
|
||||||
|
{ id: 'threads', label: 'Threads', icon: <Layers size={13} />, show: hasThreads },
|
||||||
|
{ id: 'thread-charts', label: 'Thread Charts', icon: <BarChart2 size={13} />, show: hasThreads },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '340px' }}>
|
<div className="border-t border-slate-700 bg-slate-950 flex flex-col">
|
||||||
{/* Panel header with tabs + export button */}
|
{/* Panel header with tabs + export button */}
|
||||||
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
|
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
|
||||||
{TAB_CONFIG.map(tab => (
|
{TAB_CONFIG.filter(t => t.show).map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
@@ -62,16 +78,21 @@ const MetricsPanel = () => {
|
|||||||
>
|
>
|
||||||
{tab.icon}
|
{tab.icon}
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
{tab.id === 'threads' && threadMetrics.length > 0 && (
|
||||||
|
<span className="text-[9px] bg-blue-500/20 text-blue-400 px-1 rounded">
|
||||||
|
{threadMetrics.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => exportCSV(processMetrics, processMap)}
|
onClick={() => exportCSV(processMetrics, processMap, hasThreads ? threadMetrics : undefined)}
|
||||||
disabled={!canExportCSV}
|
disabled={!canExportCSV}
|
||||||
title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como CSV'}
|
title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as CSV'}
|
||||||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
aria-label="Exportar métricas CSV"
|
aria-label="Export metrics as CSV"
|
||||||
>
|
>
|
||||||
<Download size={12} />
|
<Download size={12} />
|
||||||
CSV
|
CSV
|
||||||
@@ -80,16 +101,22 @@ const MetricsPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div>
|
||||||
{activeTab === 'table' && (
|
{activeTab === 'table' && (
|
||||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||||
)}
|
)}
|
||||||
{activeTab === 'global' && (
|
{activeTab === 'global' && (
|
||||||
<GlobalMetrics metrics={globalMetrics} />
|
<GlobalMetrics metrics={globalMetrics} />
|
||||||
)}
|
)}
|
||||||
{activeTab === 'chart' && (
|
{activeTab === 'charts' && (
|
||||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 'threads' && (
|
||||||
|
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'thread-charts' && (
|
||||||
|
<ThreadMetricsChart metrics={threadMetrics} processMap={processMap} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface MiniBarProps {
|
|||||||
const MiniBar = ({ value, max, color }: MiniBarProps) => {
|
const MiniBar = ({ value, max, color }: MiniBarProps) => {
|
||||||
const pct = max > 0 ? (value / max) * 100 : 0;
|
const pct = max > 0 ? (value / max) * 100 : 0;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-end gap-1.5 min-w-[52px]">
|
<div className="relative flex items-center justify-end gap-1.5 min-w-13">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
|
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
|
||||||
style={{ width: `${pct}%`, backgroundColor: color }}
|
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||||
@@ -36,7 +36,7 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
|||||||
if (metrics.length === 0) {
|
if (metrics.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
|
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
|
||||||
Nenhum processo concluído ainda.
|
No completed processes yet.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,10 +50,10 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-[780px]">
|
<table className="w-full text-xs text-slate-300 border-collapse min-w-195">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
|
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
|
||||||
<th className="text-left px-3 py-2 font-medium">Processo</th>
|
<th className="text-left px-3 py-2 font-medium">Process</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">Arrival</th>
|
<th className="text-right px-3 py-2 font-medium">Arrival</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">Start</th>
|
<th className="text-right px-3 py-2 font-medium">Start</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">Finish</th>
|
<th className="text-right px-3 py-2 font-medium">Finish</th>
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
CartesianGrid,
|
||||||
|
RadarChart,
|
||||||
|
PolarGrid,
|
||||||
|
PolarAngleAxis,
|
||||||
|
Radar,
|
||||||
|
} from 'recharts';
|
||||||
|
import type { ThreadMetrics, Process } from '@core/types';
|
||||||
|
import { deriveThreadColor } from '../shared/threadUtils';
|
||||||
|
|
||||||
|
interface ThreadMetricsChartProps {
|
||||||
|
metrics: ThreadMetrics[];
|
||||||
|
processMap: Record<string, Process>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLTIP_STYLE = {
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
border: '1px solid #334155',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#e2e8f0',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartBox = ({ height, children }: { height: number; children: React.ReactNode }) => (
|
||||||
|
<div style={{ width: '100%', height, overflow: 'hidden', flexShrink: 0 }}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
{children as React.ReactElement}
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ThreadMetricsChart = ({ metrics, processMap }: ThreadMetricsChartProps) => {
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
||||||
|
No data available.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compute thread colors in order of appearance
|
||||||
|
const threadIndexByProcess: Record<string, number> = {};
|
||||||
|
const threadColors: Record<string, string> = {};
|
||||||
|
metrics.forEach(m => {
|
||||||
|
if (threadIndexByProcess[m.processId] === undefined) {
|
||||||
|
threadIndexByProcess[m.processId] = 0;
|
||||||
|
}
|
||||||
|
const idx = threadIndexByProcess[m.processId]!;
|
||||||
|
const procColor = processMap[m.processId]?.color ?? '#94a3b8';
|
||||||
|
threadColors[`${m.processId}-${m.threadId}`] = deriveThreadColor(procColor, idx);
|
||||||
|
threadIndexByProcess[m.processId] = idx + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getThreadLabel = (m: ThreadMetrics) => {
|
||||||
|
const procName = processMap[m.processId]?.name ?? m.processId;
|
||||||
|
return `${procName}/${m.threadName}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThreadColor = (m: ThreadMetrics) =>
|
||||||
|
threadColors[`${m.processId}-${m.threadId}`] ?? '#94a3b8';
|
||||||
|
|
||||||
|
// ── Radar tooltip ──────────────────────────────────────────────
|
||||||
|
const labelToMetric = Object.fromEntries(metrics.map(m => [getThreadLabel(m), m]));
|
||||||
|
|
||||||
|
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
|
||||||
|
if (!active || !payload?.length) return null;
|
||||||
|
return (
|
||||||
|
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
|
||||||
|
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{payload.map((entry: any) => {
|
||||||
|
const m = labelToMetric[entry.name];
|
||||||
|
return (
|
||||||
|
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
|
||||||
|
<span style={{ color: '#cbd5e1' }}>{m ? getThreadLabel(m) : entry.name}</span>
|
||||||
|
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
|
||||||
|
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Stacked bar data ──────────────────────────────────────────
|
||||||
|
const barData = metrics.map(m => ({
|
||||||
|
name: getThreadLabel(m),
|
||||||
|
'Waiting': m.waitingTime,
|
||||||
|
'CPU': m.cpuTime,
|
||||||
|
'I/O': m.ioTime,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ── Radar chart data (normalized 0-100 per dimension) ─────────
|
||||||
|
const maxT = Math.max(...metrics.map(m => m.turnaroundTime), 1);
|
||||||
|
const maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||||
|
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||||
|
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
|
||||||
|
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
|
||||||
|
|
||||||
|
const radarDimensions = ['Turnaround', 'Response', 'Waiting', 'CPU Time', 'I/O Time'] as const;
|
||||||
|
|
||||||
|
const radarData = radarDimensions.map(dim => {
|
||||||
|
const row: Record<string, string | number> = { dimension: dim };
|
||||||
|
metrics.forEach(m => {
|
||||||
|
const key = getThreadLabel(m);
|
||||||
|
switch (dim) {
|
||||||
|
case 'Turnaround': row[key] = (m.turnaroundTime / maxT) * 100; break;
|
||||||
|
case 'Response': row[key] = (m.responseTime / maxR) * 100; break;
|
||||||
|
case 'Waiting': row[key] = (m.waitingTime / maxW) * 100; break;
|
||||||
|
case 'CPU Time': row[key] = (m.cpuTime / maxCpu) * 100; break;
|
||||||
|
case 'I/O Time': row[key] = (m.ioTime / maxIo) * 100; break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showRadar = metrics.length >= 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 flex flex-col gap-6">
|
||||||
|
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||||
|
{showRadar && (
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||||
|
Comparative Profile (normalized)
|
||||||
|
</p>
|
||||||
|
<ChartBox height={360}>
|
||||||
|
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||||
|
<PolarGrid stroke="#334155" />
|
||||||
|
<PolarAngleAxis
|
||||||
|
dataKey="dimension"
|
||||||
|
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||||
|
/>
|
||||||
|
{metrics.map(m => (
|
||||||
|
<Radar
|
||||||
|
key={`${m.processId}-${m.threadId}`}
|
||||||
|
name={getThreadLabel(m)}
|
||||||
|
dataKey={getThreadLabel(m)}
|
||||||
|
stroke={getThreadColor(m)}
|
||||||
|
fill={getThreadColor(m)}
|
||||||
|
fillOpacity={0.15}
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3, fill: getThreadColor(m) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Tooltip content={radarTooltipContent} />
|
||||||
|
<Legend
|
||||||
|
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||||
|
/>
|
||||||
|
</RadarChart>
|
||||||
|
</ChartBox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||||
|
Breakdown per Thread (ticks)
|
||||||
|
</p>
|
||||||
|
<ChartBox height={360}>
|
||||||
|
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 48 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||||
|
axisLine={{ stroke: '#334155' }}
|
||||||
|
tickLine={false}
|
||||||
|
angle={-35}
|
||||||
|
textAnchor="end"
|
||||||
|
interval={0}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fill: '#94a3b8', fontSize: 10 }}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
label={{
|
||||||
|
value: 'ticks',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
offset: 14,
|
||||||
|
style: { fill: '#475569', fontSize: 9 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={TOOLTIP_STYLE}
|
||||||
|
cursor={{ fill: 'rgba(255,255,255,0.04)' }}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="Waiting" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
|
||||||
|
<Bar dataKey="CPU" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
|
||||||
|
<Bar dataKey="I/O" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ChartBox>
|
||||||
|
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||||
|
Waiting + CPU + I/O = Turnaround
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThreadMetricsChart;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import type { Process, ThreadMetrics } from "@core/types";
|
||||||
|
import { deriveThreadColor } from "../shared/threadUtils";
|
||||||
|
|
||||||
|
interface ThreadMetricsTableProps {
|
||||||
|
metrics: ThreadMetrics[];
|
||||||
|
processMap: Record<string, Process>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MiniBar = ({
|
||||||
|
value,
|
||||||
|
max,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
max: number;
|
||||||
|
color: string;
|
||||||
|
}) => {
|
||||||
|
const pct = max > 0 ? (value / max) * 100 : 0;
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-end gap-1.5 min-w-13">
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
|
||||||
|
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||||
|
/>
|
||||||
|
<span className="relative z-10 font-mono text-xs">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThreadMetricsTable = ({ metrics, processMap }: ThreadMetricsTableProps) => {
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
|
||||||
|
No completed threads yet.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxResponse = Math.max(...metrics.map((m) => m.responseTime), 1);
|
||||||
|
const maxTurnaround = Math.max(...metrics.map((m) => m.turnaroundTime), 1);
|
||||||
|
const maxWaiting = Math.max(...metrics.map((m) => m.waitingTime), 1);
|
||||||
|
const maxCpu = Math.max(...metrics.map((m) => m.cpuTime), 1);
|
||||||
|
const maxIo = Math.max(...metrics.map((m) => m.ioTime), 1);
|
||||||
|
|
||||||
|
// Agrupa por processo para mostrar o índice de thread e cor correta
|
||||||
|
const threadIndexByProcess: Record<string, number> = {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs text-slate-300 border-collapse min-w-195">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
|
||||||
|
<th className="text-left px-3 py-2 font-medium">Thread</th>
|
||||||
|
<th className="text-left px-3 py-2 font-medium">Process</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium">Arrival</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium">Start</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium">Finish</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium text-amber-400">Response</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium text-blue-400">Turnaround</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium text-orange-400">Waiting</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium text-emerald-400">CPU</th>
|
||||||
|
<th className="text-right px-3 py-2 font-medium text-cyan-400">I/O</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{metrics.map((m, i) => {
|
||||||
|
const proc = processMap[m.processId];
|
||||||
|
const procColor = proc?.color ?? "#94a3b8";
|
||||||
|
|
||||||
|
// Obtém o índice da thread dentro do processo para cor
|
||||||
|
if (threadIndexByProcess[m.processId] === undefined) {
|
||||||
|
threadIndexByProcess[m.processId] = 0;
|
||||||
|
}
|
||||||
|
const threadIdx = threadIndexByProcess[m.processId]!;
|
||||||
|
threadIndexByProcess[m.processId] = threadIdx + 1;
|
||||||
|
|
||||||
|
const threadColor = deriveThreadColor(procColor, threadIdx);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`${m.processId}-${m.threadId}`}
|
||||||
|
className={`border-b border-slate-800/80 hover:bg-slate-800/50 transition-colors ${
|
||||||
|
i % 2 === 0 ? "" : "bg-slate-900/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Thread name */}
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="inline-block w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: threadColor }}
|
||||||
|
/>
|
||||||
|
<span className="font-medium text-slate-200">{m.threadName}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Process name */}
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="inline-block w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: procColor }}
|
||||||
|
/>
|
||||||
|
<span className="text-slate-400 text-[10px]">
|
||||||
|
{proc?.name ?? m.processId}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.arrivalTime}</td>
|
||||||
|
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.startTick}</td>
|
||||||
|
<td className="text-right px-3 py-2 font-mono text-slate-400">{m.finishTick}</td>
|
||||||
|
|
||||||
|
<td className="px-3 py-2 text-amber-300">
|
||||||
|
<MiniBar value={m.responseTime} max={maxResponse} color="#f59e0b" />
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-blue-300">
|
||||||
|
<MiniBar value={m.turnaroundTime} max={maxTurnaround} color="#60a5fa" />
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-orange-300">
|
||||||
|
<MiniBar value={m.waitingTime} max={maxWaiting} color="#fb923c" />
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-emerald-300">
|
||||||
|
<MiniBar value={m.cpuTime} max={maxCpu} color="#34d399" />
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-cyan-300">
|
||||||
|
<MiniBar value={m.ioTime} max={maxIo} color="#22d3ee" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThreadMetricsTable;
|
||||||
@@ -10,10 +10,28 @@ interface ProcessCardProps {
|
|||||||
onRemove: (id: string) => void;
|
onRemove: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MODEL_LABEL: Record<string, string> = {
|
||||||
|
MANY_TO_ONE: 'M:1',
|
||||||
|
ONE_TO_ONE: '1:1',
|
||||||
|
MANY_TO_MANY: 'M:M',
|
||||||
|
};
|
||||||
|
|
||||||
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||||
const bound = classifyBound(process.bursts);
|
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||||
const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
|
|
||||||
const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
|
const bound = hasThreads ? 'Balanced' : classifyBound(process.bursts);
|
||||||
|
const totalCpu = hasThreads
|
||||||
|
? process.threads!.reduce(
|
||||||
|
(sum, t) => sum + t.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
: process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
|
||||||
|
const totalIo = hasThreads
|
||||||
|
? process.threads!.reduce(
|
||||||
|
(sum, t) => sum + t.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
: process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
|
||||||
|
|
||||||
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
|
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
|
||||||
|
|
||||||
@@ -38,7 +56,13 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
||||||
|
{hasThreads ? (
|
||||||
|
<span className="text-[9px] font-semibold px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
|
||||||
|
{MODEL_LABEL[process.threadModel ?? 'ONE_TO_ONE']} · {process.threads!.length}T
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<Badge variant={boundVariant}>{bound}</Badge>
|
<Badge variant={boundVariant}>{bound}</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
|
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
|
||||||
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
|
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
|
||||||
@@ -47,7 +71,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
{totalIo > 0 && (
|
{totalIo > 0 && (
|
||||||
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
||||||
)}
|
)}
|
||||||
|
{hasThreads ? (
|
||||||
|
<span>Threads: <span className="text-slate-300 font-medium">{process.threads!.length}</span></span>
|
||||||
|
) : (
|
||||||
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
|
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,7 +84,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
label={`Editar ${process.name}`}
|
label={`Edit ${process.name}`}
|
||||||
onClick={() => onEdit(process)}
|
onClick={() => onEdit(process)}
|
||||||
>
|
>
|
||||||
<Pencil size={13} />
|
<Pencil size={13} />
|
||||||
@@ -64,7 +92,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
label={`Remover ${process.name}`}
|
label={`Remove ${process.name}`}
|
||||||
onClick={() => onRemove(process.id)}
|
onClick={() => onRemove(process.id)}
|
||||||
className="hover:text-red-400 hover:bg-red-900/20"
|
className="hover:text-red-400 hover:bg-red-900/20"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Minus, Check, X } from 'lucide-react';
|
import { Plus, Minus, Check, X } from 'lucide-react';
|
||||||
import type { BurstSegment, Process } from '@core/types';
|
import type { BurstSegment, IntraProcessPolicy, Process, Thread, ThreadModel } from '@core/types';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
import { useProcessStore } from '../../store/processStore';
|
||||||
import IconButton from '../shared/IconButton';
|
import IconButton from '../shared/IconButton';
|
||||||
import { generateId, nextColor } from '../shared/processUtils';
|
import { generateId, nextColor } from '../shared/processUtils';
|
||||||
|
import ThreadEditor from './ThreadEditor';
|
||||||
|
|
||||||
interface ProcessFormProps {
|
interface ProcessFormProps {
|
||||||
/** If provided, the form edits this process; otherwise creates a new one. */
|
|
||||||
editingProcess?: Process | null;
|
editingProcess?: Process | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FormTab = 'bursts' | 'threads';
|
||||||
|
|
||||||
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
||||||
|
|
||||||
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||||
@@ -19,12 +21,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
|
|
||||||
const isEditing = !!editingProcess;
|
const isEditing = !!editingProcess;
|
||||||
|
|
||||||
|
// ── Campos base ─────────────────────────────────────────────────────────
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [arrivalTime, setArrivalTime] = useState(0);
|
const [arrivalTime, setArrivalTime] = useState(0);
|
||||||
const [priority, setPriority] = useState(5);
|
const [priority, setPriority] = useState(5);
|
||||||
const [color, setColor] = useState(nextColor);
|
const [color, setColor] = useState(nextColor);
|
||||||
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
||||||
const [errors, setErrors] = useState<string[]>([]);
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
|
const [, setActiveTab] = useState<FormTab>('bursts');
|
||||||
|
|
||||||
|
// ── Campos de threads ────────────────────────────────────────────────────
|
||||||
|
const [hasThreads, setHasThreads] = useState(false);
|
||||||
|
const [threads, setThreads] = useState<Thread[]>([]);
|
||||||
|
const [threadModel, setThreadModel] = useState<ThreadModel>('ONE_TO_ONE');
|
||||||
|
const [threadPolicy, setThreadPolicy] = useState<IntraProcessPolicy>('FCFS');
|
||||||
|
const [threadQuantum, setThreadQuantum] = useState(2);
|
||||||
|
const [kernelThreadCount, setKernelThreadCount] = useState(2);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingProcess) {
|
if (editingProcess) {
|
||||||
@@ -33,24 +45,38 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
setPriority(editingProcess.priority);
|
setPriority(editingProcess.priority);
|
||||||
setColor(editingProcess.color);
|
setColor(editingProcess.color);
|
||||||
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
|
setBursts([...editingProcess.bursts.map((b: BurstSegment) => ({ ...b }))]);
|
||||||
|
|
||||||
|
const hasT = (editingProcess.threads?.length ?? 0) > 0;
|
||||||
|
setHasThreads(hasT);
|
||||||
|
setThreads(editingProcess.threads ? [...editingProcess.threads] : []);
|
||||||
|
setThreadModel(editingProcess.threadModel ?? 'ONE_TO_ONE');
|
||||||
|
setThreadPolicy(editingProcess.threadSchedulingPolicy ?? 'FCFS');
|
||||||
|
setThreadQuantum(editingProcess.threadQuantum ?? 2);
|
||||||
|
setKernelThreadCount(editingProcess.kernelThreadCount ?? 2);
|
||||||
|
setActiveTab(hasT ? 'threads' : 'bursts');
|
||||||
} else {
|
} else {
|
||||||
setName('');
|
setName('');
|
||||||
setArrivalTime(0);
|
setArrivalTime(0);
|
||||||
setPriority(5);
|
setPriority(5);
|
||||||
setColor(nextColor());
|
setColor(nextColor());
|
||||||
setBursts([{ type: 'cpu', duration: 4 }]);
|
setBursts([{ type: 'cpu', duration: 4 }]);
|
||||||
|
setHasThreads(false);
|
||||||
|
setThreads([]);
|
||||||
|
setThreadModel('ONE_TO_ONE');
|
||||||
|
setThreadPolicy('FCFS');
|
||||||
|
setThreadQuantum(2);
|
||||||
|
setKernelThreadCount(2);
|
||||||
|
setActiveTab('bursts');
|
||||||
}
|
}
|
||||||
setErrors([]);
|
setErrors([]);
|
||||||
}, [editingProcess]);
|
}, [editingProcess]);
|
||||||
|
|
||||||
// ---- Burst helpers ----
|
// ── Helpers de bursts ───────────────────────────────────────────────────
|
||||||
|
|
||||||
const updateBurstDuration = (index: number, value: number) => {
|
const updateBurstDuration = (index: number, value: number) => {
|
||||||
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
||||||
};
|
};
|
||||||
|
|
||||||
const addBurst = () => {
|
const addBurst = () => {
|
||||||
// enforce alternating cpu/io pattern
|
|
||||||
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||||
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||||
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
||||||
@@ -58,36 +84,90 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
|
|
||||||
const removeBurst = (index: number) => {
|
const removeBurst = (index: number) => {
|
||||||
if (bursts.length <= 1) return;
|
if (bursts.length <= 1) return;
|
||||||
setBursts(prev => {
|
setBursts(prev => prev.filter((_, i) => i !== index));
|
||||||
const next = prev.filter((_, i) => i !== index);
|
|
||||||
// Safe-guard: ensure it still starts (and ends) with cpu
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Validation ----
|
const toggleThreads = (enabled: boolean) => {
|
||||||
|
setHasThreads(enabled);
|
||||||
|
if (enabled && threads.length === 0) {
|
||||||
|
setThreads([
|
||||||
|
{ tid: `t_${generateId()}`, name: 'T1', priority: 5, bursts: [{ type: 'cpu', duration: 3 }] },
|
||||||
|
{ tid: `t_${generateId()}`, name: 'T2', priority: 5, bursts: [{ type: 'cpu', duration: 2 }] },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
setActiveTab(enabled ? 'threads' : 'bursts');
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Validação ───────────────────────────────────────────────────────────
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errs: string[] = [];
|
const errs: string[] = [];
|
||||||
if (!name.trim()) errs.push('Nome é obrigatório');
|
if (!name.trim()) errs.push('Name is required');
|
||||||
if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres');
|
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
|
||||||
if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0');
|
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
|
||||||
if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10');
|
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
||||||
if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU');
|
|
||||||
if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU');
|
if (hasThreads) {
|
||||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
|
if (threads.length === 0) errs.push('At least one thread is required');
|
||||||
bursts.forEach((b, i) => {
|
threads.forEach((t, ti) => {
|
||||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
|
if (!t.name.trim()) errs.push(`Thread ${ti + 1}: name is required`);
|
||||||
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
|
if (t.bursts.length === 0) errs.push(`Thread ${ti + 1}: at least 1 burst required`);
|
||||||
|
if (t.bursts[0]?.type !== 'cpu') errs.push(`Thread ${ti + 1}: first burst must be CPU`);
|
||||||
|
if (t.bursts.at(-1)?.type !== 'cpu') errs.push(`Thread ${ti + 1}: last burst must be CPU`);
|
||||||
|
t.bursts.forEach((b, bi) => {
|
||||||
|
if (b.duration < 1) errs.push(`Thread ${ti + 1}, burst ${bi + 1}: duration must be ≥ 1`);
|
||||||
|
if (bi > 0 && t.bursts[bi - 1]?.type === b.type)
|
||||||
|
errs.push(`Thread ${ti + 1}: bursts must alternate CPU/IO`);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
if (threadModel === 'MANY_TO_MANY' && kernelThreadCount > threads.length) {
|
||||||
|
errs.push(`Kernel threads (${kernelThreadCount}) cannot exceed user threads (${threads.length})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
|
||||||
|
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
|
||||||
|
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
||||||
|
bursts.forEach((b, i) => {
|
||||||
|
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
||||||
|
if (i > 0 && bursts[i - 1]?.type === b.type)
|
||||||
|
errs.push(`Bursts ${i} and ${i + 1} cannot be the same type`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setErrors(errs);
|
setErrors(errs);
|
||||||
return errs.length === 0;
|
return errs.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!validate()) return;
|
if (!validate()) return;
|
||||||
|
|
||||||
|
const processData: Partial<Process> = {
|
||||||
|
name: name.trim(),
|
||||||
|
arrivalTime,
|
||||||
|
priority,
|
||||||
|
color,
|
||||||
|
bursts: hasThreads ? [] : bursts,
|
||||||
|
...(hasThreads
|
||||||
|
? {
|
||||||
|
threads,
|
||||||
|
threadModel,
|
||||||
|
threadSchedulingPolicy: threadPolicy,
|
||||||
|
threadQuantum,
|
||||||
|
kernelThreadCount:
|
||||||
|
threadModel === 'MANY_TO_MANY'
|
||||||
|
? kernelThreadCount
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
threads: undefined,
|
||||||
|
threadModel: undefined,
|
||||||
|
threadSchedulingPolicy: undefined,
|
||||||
|
threadQuantum: undefined,
|
||||||
|
kernelThreadCount: undefined,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if (isEditing && editingProcess) {
|
if (isEditing && editingProcess) {
|
||||||
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
updateProcess(editingProcess.id, processData);
|
||||||
} else {
|
} else {
|
||||||
addProcess({
|
addProcess({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -95,7 +175,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
arrivalTime,
|
arrivalTime,
|
||||||
priority,
|
priority,
|
||||||
color,
|
color,
|
||||||
bursts,
|
bursts: hasThreads ? [] : bursts,
|
||||||
|
...(hasThreads
|
||||||
|
? {
|
||||||
|
threads,
|
||||||
|
threadModel,
|
||||||
|
threadSchedulingPolicy: threadPolicy,
|
||||||
|
threadQuantum,
|
||||||
|
kernelThreadCount:
|
||||||
|
threadModel === 'MANY_TO_MANY' ? kernelThreadCount : undefined,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@@ -108,9 +198,9 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
<div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form">
|
<div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-slate-200">
|
<h3 className="text-sm font-semibold text-slate-200">
|
||||||
{isEditing ? 'Editar Processo' : 'Novo Processo'}
|
{isEditing ? 'Edit Process' : 'New Process'}
|
||||||
</h3>
|
</h3>
|
||||||
<IconButton size="sm" variant="ghost" label="Fechar" onClick={onClose}>
|
<IconButton size="sm" variant="ghost" label="Close" onClick={onClose}>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,9 +208,8 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
{/* Basic fields */}
|
{/* Basic fields */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="col-span-2 flex gap-3">
|
<div className="col-span-2 flex gap-3">
|
||||||
{/* Name */}
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className={labelCls} htmlFor="pf-name">Nome</label>
|
<label className={labelCls} htmlFor="pf-name">Name</label>
|
||||||
<input
|
<input
|
||||||
id="pf-name"
|
id="pf-name"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -129,24 +218,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
onChange={e => setName(e.target.value)}
|
onChange={e => setName(e.target.value)}
|
||||||
placeholder="P1"
|
placeholder="P1"
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
aria-label="Nome do processo"
|
aria-label="Process name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Color */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls} htmlFor="pf-color">Cor</label>
|
<label className={labelCls} htmlFor="pf-color">Color</label>
|
||||||
<input
|
<input
|
||||||
id="pf-color"
|
id="pf-color"
|
||||||
type="color"
|
type="color"
|
||||||
value={color}
|
value={color}
|
||||||
onChange={e => setColor(e.target.value)}
|
onChange={e => setColor(e.target.value)}
|
||||||
className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
|
className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
|
||||||
aria-label="Cor do processo"
|
aria-label="Process color"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Arrival */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
||||||
<input
|
<input
|
||||||
@@ -156,11 +243,10 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
value={arrivalTime}
|
value={arrivalTime}
|
||||||
onChange={e => setArrivalTime(Number(e.target.value))}
|
onChange={e => setArrivalTime(Number(e.target.value))}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
aria-label="Tempo de chegada"
|
aria-label="Arrival time"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Priority */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -171,16 +257,40 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
value={priority}
|
value={priority}
|
||||||
onChange={e => setPriority(Number(e.target.value))}
|
onChange={e => setPriority(Number(e.target.value))}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
aria-label="Prioridade do processo"
|
aria-label="Process priority"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bursts editor */}
|
{/* Toggle threads */}
|
||||||
|
<div className="flex items-center justify-between border-t border-slate-700 pt-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-slate-300 font-medium">Use Threads</span>
|
||||||
|
<p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={hasThreads}
|
||||||
|
onClick={() => toggleThreads(!hasThreads)}
|
||||||
|
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||||
|
hasThreads ? 'bg-blue-600' : 'bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||||
|
hasThreads ? 'translate-x-4' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs: Bursts / Threads */}
|
||||||
|
{!hasThreads ? (
|
||||||
|
/* ── Editor de bursts (original) ─────────────────────────── */
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||||
<IconButton size="sm" variant="ghost" label="Adicionar burst" onClick={addBurst}>
|
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
|
||||||
<Plus size={13} />
|
<Plus size={13} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,13 +313,13 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
value={burst.duration}
|
value={burst.duration}
|
||||||
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
||||||
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
aria-label={`Duração do burst ${idx + 1}`}
|
aria-label={`Burst ${idx + 1} duration`}
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-slate-500">ticks</span>
|
<span className="text-[10px] text-slate-500">ticks</span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
label={`Remover burst ${idx + 1}`}
|
label={`Remove burst ${idx + 1}`}
|
||||||
onClick={() => removeBurst(idx)}
|
onClick={() => removeBurst(idx)}
|
||||||
disabled={bursts.length <= 1}
|
disabled={bursts.length <= 1}
|
||||||
className="hover:text-red-400"
|
className="hover:text-red-400"
|
||||||
@@ -220,6 +330,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* ── Editor de threads ────────────────────────────────────── */
|
||||||
|
<ThreadEditor
|
||||||
|
threads={threads}
|
||||||
|
threadModel={threadModel}
|
||||||
|
threadSchedulingPolicy={threadPolicy}
|
||||||
|
threadQuantum={threadQuantum}
|
||||||
|
kernelThreadCount={kernelThreadCount}
|
||||||
|
processColor={color}
|
||||||
|
onChange={setThreads}
|
||||||
|
onModelChange={setThreadModel}
|
||||||
|
onPolicyChange={setThreadPolicy}
|
||||||
|
onQuantumChange={setThreadQuantum}
|
||||||
|
onKernelCountChange={setKernelThreadCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Validation errors */}
|
{/* Validation errors */}
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
@@ -235,17 +361,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors"
|
className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors"
|
||||||
aria-label={isEditing ? 'Salvar processo' : 'Criar processo'}
|
aria-label={isEditing ? 'Save process' : 'Create process'}
|
||||||
>
|
>
|
||||||
<Check size={15} />
|
<Check size={15} />
|
||||||
{isEditing ? 'Salvar' : 'Criar'}
|
{isEditing ? 'Save' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
|
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
|
||||||
aria-label="Cancelar"
|
aria-label="Cancel"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
|||||||
{/* List header */}
|
{/* List header */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-400">
|
||||||
{processes.length}/{MAX_PROCESSES} processos
|
{processes.length}/{MAX_PROCESSES} processes
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
label="Gerar processo aleatório"
|
label="Generate random process"
|
||||||
onClick={handleRandomize}
|
onClick={handleRandomize}
|
||||||
disabled={!canAdd}
|
disabled={!canAdd}
|
||||||
>
|
>
|
||||||
@@ -62,7 +62,7 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
label="Limpar todos os processos"
|
label="Clear all processes"
|
||||||
onClick={clearProcesses}
|
onClick={clearProcesses}
|
||||||
disabled={processes.length === 0}
|
disabled={processes.length === 0}
|
||||||
className="hover:text-red-400 hover:bg-red-900/20"
|
className="hover:text-red-400 hover:bg-red-900/20"
|
||||||
@@ -76,8 +76,8 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
|||||||
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
|
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
|
||||||
{processes.length === 0 ? (
|
{processes.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg">
|
<div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg">
|
||||||
<span>Nenhum processo adicionado</span>
|
<span>No processes added</span>
|
||||||
<span className="text-xs">Clique em + para criar</span>
|
<span className="text-xs">Click + to create</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
processes.map(p => (
|
processes.map(p => (
|
||||||
@@ -95,11 +95,11 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
|||||||
{canAdd && (
|
{canAdd && (
|
||||||
<button
|
<button
|
||||||
onClick={onAddNew}
|
onClick={onAddNew}
|
||||||
aria-label="Adicionar novo processo"
|
aria-label="Add new process"
|
||||||
className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors"
|
className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors"
|
||||||
>
|
>
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
Adicionar Processo
|
Add Process
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ const ProcessPanel = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="Painel de Processos">
|
<section aria-label="Processes Panel">
|
||||||
<div className="flex items-center gap-2 mb-3 text-slate-300">
|
<div className="flex items-center gap-2 mb-3 text-slate-300">
|
||||||
<ListTree size={18} className="text-blue-400" />
|
<ListTree size={18} className="text-blue-400" />
|
||||||
<h2 className="font-semibold uppercase text-xs tracking-wider">Processos</h2>
|
<h2 className="font-semibold uppercase text-xs tracking-wider">Processes</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showForm ? (
|
{showForm ? (
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Plus, Minus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import type { BurstSegment, IntraProcessPolicy, Thread, ThreadModel } from '@core/types';
|
||||||
|
import { generateId } from '../shared/processUtils';
|
||||||
|
import { deriveThreadColor } from '../shared/threadUtils';
|
||||||
|
import IconButton from '../shared/IconButton';
|
||||||
|
|
||||||
|
interface ThreadEditorProps {
|
||||||
|
threads: Thread[];
|
||||||
|
threadModel: ThreadModel;
|
||||||
|
threadSchedulingPolicy: IntraProcessPolicy;
|
||||||
|
threadQuantum: number;
|
||||||
|
kernelThreadCount: number;
|
||||||
|
processColor: string;
|
||||||
|
onChange: (threads: Thread[]) => void;
|
||||||
|
onModelChange: (model: ThreadModel) => void;
|
||||||
|
onPolicyChange: (policy: IntraProcessPolicy) => void;
|
||||||
|
onQuantumChange: (q: number) => void;
|
||||||
|
onKernelCountChange: (k: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODEL_OPTIONS: { value: ThreadModel; label: string; desc: string }[] = [
|
||||||
|
{
|
||||||
|
value: 'MANY_TO_ONE',
|
||||||
|
label: 'Many-to-One',
|
||||||
|
desc: 'N threads → 1 kernel. I/O bloqueia todo o processo (green threads).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ONE_TO_ONE',
|
||||||
|
label: 'One-to-One',
|
||||||
|
desc: 'N threads → N kernel threads. Bloqueios são independentes (pthreads).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'MANY_TO_MANY',
|
||||||
|
label: 'Many-to-Many',
|
||||||
|
desc: 'N threads → M kernel threads (M ≤ N). Slots liberados em I/O.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const POLICY_OPTIONS: { value: IntraProcessPolicy; label: string }[] = [
|
||||||
|
{ value: 'FCFS', label: 'FCFS' },
|
||||||
|
{ value: 'RR', label: 'Round Robin' },
|
||||||
|
{ value: 'PRIORITY', label: 'Priority' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
||||||
|
const labelCls = 'block text-[10px] text-slate-400 mb-0.5';
|
||||||
|
|
||||||
|
// ── Burst editor interna de cada thread ───────────────────────────────────
|
||||||
|
const ThreadBurstEditor = ({
|
||||||
|
bursts,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
bursts: BurstSegment[];
|
||||||
|
onChange: (b: BurstSegment[]) => void;
|
||||||
|
}) => {
|
||||||
|
const addBurst = () => {
|
||||||
|
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||||
|
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||||
|
onChange([...bursts, { type: nextType, duration: 2 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBurst = (idx: number) => {
|
||||||
|
if (bursts.length <= 1) return;
|
||||||
|
onChange(bursts.filter((_, i) => i !== idx));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDuration = (idx: number, val: number) => {
|
||||||
|
onChange(bursts.map((b, i) => (i === idx ? { ...b, duration: Math.max(1, val) } : b)));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1 mt-1.5">
|
||||||
|
{bursts.map((b, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`flex items-center gap-1.5 px-2 py-1 rounded border ${
|
||||||
|
b.type === 'cpu'
|
||||||
|
? 'border-emerald-700/40 bg-emerald-900/15'
|
||||||
|
: 'border-cyan-700/40 bg-cyan-900/15'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`text-[9px] font-bold uppercase w-6 tracking-wide shrink-0 ${b.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
|
||||||
|
{b.type}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={b.duration}
|
||||||
|
onChange={e => updateDuration(idx, Number(e.target.value))}
|
||||||
|
className="w-14 rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-[10px] px-1.5 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-[9px] text-slate-500">ticks</span>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
label="Remove burst"
|
||||||
|
onClick={() => removeBurst(idx)}
|
||||||
|
disabled={bursts.length <= 1}
|
||||||
|
className="ml-auto hover:text-red-400"
|
||||||
|
>
|
||||||
|
<Minus size={10} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={addBurst}
|
||||||
|
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus size={10} /> burst
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Card de thread individual ─────────────────────────────────────────────
|
||||||
|
const ThreadCard = ({
|
||||||
|
thread,
|
||||||
|
index,
|
||||||
|
processColor,
|
||||||
|
onUpdate,
|
||||||
|
onRemove,
|
||||||
|
canRemove,
|
||||||
|
}: {
|
||||||
|
thread: Thread;
|
||||||
|
index: number;
|
||||||
|
processColor: string;
|
||||||
|
onUpdate: (t: Thread) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
canRemove: boolean;
|
||||||
|
}) => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const color = deriveThreadColor(processColor, index);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-slate-600 bg-slate-800/50 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 px-2.5 py-1.5">
|
||||||
|
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: color }} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
maxLength={8}
|
||||||
|
value={thread.name}
|
||||||
|
onChange={e => onUpdate({ ...thread, name: e.target.value })}
|
||||||
|
className="flex-1 bg-transparent text-slate-200 text-xs font-medium focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
|
||||||
|
aria-label={`Thread ${index + 1} name`}
|
||||||
|
/>
|
||||||
|
<span className="text-[9px] text-slate-500 font-mono shrink-0">T{index + 1}</span>
|
||||||
|
|
||||||
|
{/* Priority */}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
title="Priority (1=high, 10=low)"
|
||||||
|
value={thread.priority ?? 5}
|
||||||
|
onChange={e => onUpdate({ ...thread, priority: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
||||||
|
className="w-10 rounded bg-slate-700 border border-slate-600 text-slate-200 text-[10px] px-1 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500 text-center"
|
||||||
|
aria-label={`Thread ${index + 1} priority`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(v => !v)}
|
||||||
|
className="text-slate-500 hover:text-slate-300 transition-colors"
|
||||||
|
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||||
|
>
|
||||||
|
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
label="Remove thread"
|
||||||
|
onClick={onRemove}
|
||||||
|
disabled={!canRemove}
|
||||||
|
className="hover:text-red-400"
|
||||||
|
>
|
||||||
|
<Trash2 size={11} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className="px-2.5 pb-2 border-t border-slate-700/50">
|
||||||
|
<ThreadBurstEditor
|
||||||
|
bursts={thread.bursts}
|
||||||
|
onChange={bursts => onUpdate({ ...thread, bursts })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── ThreadEditor principal ────────────────────────────────────────────────
|
||||||
|
const ThreadEditor = ({
|
||||||
|
threads,
|
||||||
|
threadModel,
|
||||||
|
threadSchedulingPolicy,
|
||||||
|
threadQuantum,
|
||||||
|
kernelThreadCount,
|
||||||
|
processColor,
|
||||||
|
onChange,
|
||||||
|
onModelChange,
|
||||||
|
onPolicyChange,
|
||||||
|
onQuantumChange,
|
||||||
|
onKernelCountChange,
|
||||||
|
}: ThreadEditorProps) => {
|
||||||
|
const addThread = () => {
|
||||||
|
const newThread: Thread = {
|
||||||
|
tid: `t_${generateId()}`,
|
||||||
|
name: `T${threads.length + 1}`,
|
||||||
|
priority: 5,
|
||||||
|
bursts: [{ type: 'cpu', duration: 3 }],
|
||||||
|
};
|
||||||
|
onChange([...threads, newThread]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateThread = (idx: number, updated: Thread) => {
|
||||||
|
onChange(threads.map((t, i) => (i === idx ? updated : t)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeThread = (idx: number) => {
|
||||||
|
onChange(threads.filter((_, i) => i !== idx));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedModel = MODEL_OPTIONS.find(m => m.value === threadModel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Modelo de threads */}
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Thread Model</label>
|
||||||
|
<select
|
||||||
|
value={threadModel}
|
||||||
|
onChange={e => onModelChange(e.target.value as ThreadModel)}
|
||||||
|
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||||
|
aria-label="Thread model"
|
||||||
|
>
|
||||||
|
{MODEL_OPTIONS.map(m => (
|
||||||
|
<option key={m.value} value={m.value}>{m.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{selectedModel && (
|
||||||
|
<p className="text-[10px] text-slate-500 mt-1">{selectedModel.desc}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kernel thread count — apenas MANY_TO_MANY */}
|
||||||
|
{threadModel === 'MANY_TO_MANY' && (
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>
|
||||||
|
Kernel Threads (M ≤ {threads.length || '?'})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={Math.max(1, threads.length)}
|
||||||
|
value={kernelThreadCount}
|
||||||
|
onChange={e =>
|
||||||
|
onKernelCountChange(Math.min(threads.length || 1, Math.max(1, Number(e.target.value))))
|
||||||
|
}
|
||||||
|
className={inputCls}
|
||||||
|
aria-label="Kernel thread count"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Política intra-processo */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Intra-process Policy</label>
|
||||||
|
<select
|
||||||
|
value={threadSchedulingPolicy}
|
||||||
|
onChange={e => onPolicyChange(e.target.value as IntraProcessPolicy)}
|
||||||
|
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||||
|
aria-label="Thread scheduling policy"
|
||||||
|
>
|
||||||
|
{POLICY_OPTIONS.map(p => (
|
||||||
|
<option key={p.value} value={p.value}>{p.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{threadSchedulingPolicy === 'RR' && (
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Thread Quantum</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={threadQuantum}
|
||||||
|
onChange={e => onQuantumChange(Math.max(1, Number(e.target.value)))}
|
||||||
|
className={inputCls}
|
||||||
|
aria-label="Thread quantum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de threads */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
|
<label className="text-[10px] text-slate-400 font-medium">
|
||||||
|
Threads ({threads.length})
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={addThread}
|
||||||
|
disabled={threads.length >= 8}
|
||||||
|
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||||
|
aria-label="Add thread"
|
||||||
|
>
|
||||||
|
<Plus size={11} /> Thread
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{threads.map((thread, idx) => (
|
||||||
|
<ThreadCard
|
||||||
|
key={thread.tid}
|
||||||
|
thread={thread}
|
||||||
|
index={idx}
|
||||||
|
processColor={processColor}
|
||||||
|
onUpdate={updated => updateThread(idx, updated)}
|
||||||
|
onRemove={() => removeThread(idx)}
|
||||||
|
canRemove={threads.length > 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{threads.length === 0 && (
|
||||||
|
<div className="text-center py-4 text-[10px] text-slate-500">
|
||||||
|
Nenhuma thread. Clique em "+ Thread" para adicionar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThreadEditor;
|
||||||
@@ -3,14 +3,15 @@ import { useProcessStore } from '../../store/processStore';
|
|||||||
|
|
||||||
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?: boolean }[] = [
|
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?: boolean }[] = [
|
||||||
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
|
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
|
||||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
|
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (Non-preemptive)' },
|
||||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
|
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (Preemptive)' },
|
||||||
{ value: 'RR', label: 'RR — Round Robin' },
|
{ value: 'RR', label: 'RR — Round Robin' },
|
||||||
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
|
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
||||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
|
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
||||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
|
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
||||||
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
||||||
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
|
{ value: 'MULTILEVEL', label: 'MLQ — Multilevel Queue' },
|
||||||
|
{ value: 'MLFQ', label: 'MLFQ — Multilevel Feedback Queue' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const AlgorithmSelector = () => {
|
const AlgorithmSelector = () => {
|
||||||
@@ -23,14 +24,14 @@ const AlgorithmSelector = () => {
|
|||||||
htmlFor="algorithm-select"
|
htmlFor="algorithm-select"
|
||||||
className="block text-xs text-slate-400 mb-1"
|
className="block text-xs text-slate-400 mb-1"
|
||||||
>
|
>
|
||||||
Algoritmo
|
Algorithm
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="algorithm-select"
|
id="algorithm-select"
|
||||||
value={algorithm}
|
value={algorithm}
|
||||||
onChange={e => setConfig({ algorithm: e.target.value as SchedulingAlgorithm })}
|
onChange={e => setConfig({ algorithm: e.target.value as SchedulingAlgorithm })}
|
||||||
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||||
aria-label="Selecionar algoritmo de escalonamento"
|
aria-label="Select scheduling algorithm"
|
||||||
>
|
>
|
||||||
{ALGORITHM_OPTIONS.map(opt => (
|
{ALGORITHM_OPTIONS.map(opt => (
|
||||||
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { useProcessStore } from '../../store/processStore';
|
import { useProcessStore } from '../../store/processStore';
|
||||||
import type { MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
import type { MlfqLevelDef, MlqQueueDef, SchedulingAlgorithm } from '@core/types';
|
||||||
|
import { MLFQAlgorithm } from '@core/algorithms/mlfq';
|
||||||
|
|
||||||
// Algorithms that require a global quantum parameter
|
// Algorithms that require a global quantum parameter
|
||||||
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
||||||
@@ -9,7 +10,7 @@ const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR']);
|
|||||||
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
|
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P', 'PRIORITY_RR']);
|
||||||
|
|
||||||
// Algorithms that are preemptive by definition (used for read-only info only)
|
// Algorithms that are preemptive by definition (used for read-only info only)
|
||||||
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL']);
|
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL', 'MLFQ']);
|
||||||
|
|
||||||
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
||||||
const labelCls = 'block text-xs text-slate-400 mb-1';
|
const labelCls = 'block text-xs text-slate-400 mb-1';
|
||||||
@@ -71,14 +72,14 @@ const MlqQueuesConfig = ({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2 pt-1" data-testid="mlq-queues-config">
|
<div className="space-y-2 pt-1" data-testid="mlq-queues-config">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-slate-400 font-medium">Filas MLQ (prioridade decrescente)</span>
|
<span className="text-xs text-slate-400 font-medium">MLQ Queues (decreasing priority)</span>
|
||||||
<button
|
<button
|
||||||
onClick={addQueue}
|
onClick={addQueue}
|
||||||
disabled={queues.length >= 5}
|
disabled={queues.length >= 5}
|
||||||
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||||
aria-label="Adicionar fila"
|
aria-label="Add queue"
|
||||||
>
|
>
|
||||||
<Plus size={11} /> Fila
|
<Plus size={11} /> Queue
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{queues.map((q, idx) => (
|
{queues.map((q, idx) => (
|
||||||
@@ -89,46 +90,46 @@ const MlqQueuesConfig = ({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
|
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
|
||||||
Fila {idx} {idx === 0 ? '(mais alta)' : idx === queues.length - 1 ? '(mais baixa)' : ''}
|
Queue {idx} {idx === 0 ? '(highest)' : idx === queues.length - 1 ? '(lowest)' : ''}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeQueue(idx)}
|
onClick={() => removeQueue(idx)}
|
||||||
disabled={queues.length <= 1}
|
disabled={queues.length <= 1}
|
||||||
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
|
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
|
||||||
aria-label={`Remover fila ${idx}`}
|
aria-label={`Remove queue ${idx}`}
|
||||||
>
|
>
|
||||||
<Trash2 size={11} />
|
<Trash2 size={11} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. mín</label>
|
<label className="block text-[10px] text-slate-500 mb-0.5">Min. Prio</label>
|
||||||
<input
|
<input
|
||||||
type="number" min={1} max={10}
|
type="number" min={1} max={10}
|
||||||
value={q.priorityMin}
|
value={q.priorityMin}
|
||||||
onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
||||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
aria-label={`Fila ${idx}: prioridade mínima`}
|
aria-label={`Queue ${idx}: minimum priority`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. máx</label>
|
<label className="block text-[10px] text-slate-500 mb-0.5">Max. Prio</label>
|
||||||
<input
|
<input
|
||||||
type="number" min={1} max={10}
|
type="number" min={1} max={10}
|
||||||
value={q.priorityMax}
|
value={q.priorityMax}
|
||||||
onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
||||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
aria-label={`Fila ${idx}: prioridade máxima`}
|
aria-label={`Queue ${idx}: maximum priority`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] text-slate-500 mb-0.5">Algoritmo</label>
|
<label className="block text-[10px] text-slate-500 mb-0.5">Algorithm</label>
|
||||||
<select
|
<select
|
||||||
value={q.algorithm}
|
value={q.algorithm}
|
||||||
onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })}
|
onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })}
|
||||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||||
aria-label={`Fila ${idx}: algoritmo`}
|
aria-label={`Queue ${idx}: algorithm`}
|
||||||
>
|
>
|
||||||
{MLQ_ALGO_OPTIONS.map(a => (
|
{MLQ_ALGO_OPTIONS.map(a => (
|
||||||
<option key={a} value={a}>{a}</option>
|
<option key={a} value={a}>{a}</option>
|
||||||
@@ -153,6 +154,94 @@ const MlqQueuesConfig = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MlfqLevelsConfig = ({
|
||||||
|
levels,
|
||||||
|
boostInterval,
|
||||||
|
onLevelsChange,
|
||||||
|
onBoostChange,
|
||||||
|
}: {
|
||||||
|
levels: MlfqLevelDef[];
|
||||||
|
boostInterval: number;
|
||||||
|
onLevelsChange: (levels: MlfqLevelDef[]) => void;
|
||||||
|
onBoostChange: (v: number) => void;
|
||||||
|
}) => {
|
||||||
|
const update = (idx: number, quantum: number) => {
|
||||||
|
onLevelsChange(levels.map((l, i) => (i === idx ? { quantum } : l)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addLevel = () => {
|
||||||
|
if (levels.length >= 5) return;
|
||||||
|
onLevelsChange([...levels, { quantum: levels[levels.length - 1]!.quantum * 2 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLevel = (idx: number) => {
|
||||||
|
if (levels.length <= 1) return;
|
||||||
|
onLevelsChange(levels.filter((_, i) => i !== idx));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 pt-1" data-testid="mlfq-levels-config">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-slate-400 font-medium">MLFQ Levels (decreasing priority)</span>
|
||||||
|
<button
|
||||||
|
onClick={addLevel}
|
||||||
|
disabled={levels.length >= 5}
|
||||||
|
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||||
|
aria-label="Add MLFQ level"
|
||||||
|
>
|
||||||
|
<Plus size={11} /> Level
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{levels.map((l, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="rounded-md border border-slate-600 bg-slate-800/60 p-2 space-y-1.5"
|
||||||
|
data-testid={`mlfq-level-${idx}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
|
||||||
|
Level {idx} {idx === 0 ? '(highest)' : idx === levels.length - 1 ? '(lowest)' : ''}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeLevel(idx)}
|
||||||
|
disabled={levels.length <= 1}
|
||||||
|
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
|
||||||
|
aria-label={`Remove level ${idx}`}
|
||||||
|
>
|
||||||
|
<Trash2 size={11} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-slate-500 mb-0.5">Quantum (ticks)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={l.quantum}
|
||||||
|
onChange={e => update(idx, Math.max(1, Number(e.target.value)))}
|
||||||
|
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
aria-label={`Level ${idx}: quantum`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-slate-500 mb-0.5">
|
||||||
|
Boost Interval (ticks, 0 = disabled)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={boostInterval}
|
||||||
|
onChange={e => onBoostChange(Math.max(0, Number(e.target.value)))}
|
||||||
|
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
aria-label="MLFQ boost interval"
|
||||||
|
data-testid="mlfq-boost-interval"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SchedulerConfig = () => {
|
const SchedulerConfig = () => {
|
||||||
const config = useProcessStore(s => s.config);
|
const config = useProcessStore(s => s.config);
|
||||||
const setConfig = useProcessStore(s => s.setConfig);
|
const setConfig = useProcessStore(s => s.setConfig);
|
||||||
@@ -167,7 +256,7 @@ const SchedulerConfig = () => {
|
|||||||
|
|
||||||
{/* Read-only preemptive indicator */}
|
{/* Read-only preemptive indicator */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-slate-400">Tipo de escalonamento</span>
|
<span className="text-xs text-slate-400">Scheduling type</span>
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
|
className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
|
||||||
isPreemptive
|
isPreemptive
|
||||||
@@ -176,7 +265,7 @@ const SchedulerConfig = () => {
|
|||||||
}`}
|
}`}
|
||||||
data-testid="preemptive-badge"
|
data-testid="preemptive-badge"
|
||||||
>
|
>
|
||||||
{isPreemptive ? 'Preemptivo' : 'Não-preemptivo'}
|
{isPreemptive ? 'Preemptive' : 'Non-preemptive'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -193,7 +282,7 @@ const SchedulerConfig = () => {
|
|||||||
value={config.contextSwitchTime}
|
value={config.contextSwitchTime}
|
||||||
onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })}
|
onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
aria-label="Tempo de context switch"
|
aria-label="Context switch time"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,7 +299,7 @@ const SchedulerConfig = () => {
|
|||||||
value={config.quantum}
|
value={config.quantum}
|
||||||
onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })}
|
onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
aria-label="Quantum do Round Robin"
|
aria-label="Round Robin quantum"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -227,7 +316,7 @@ const SchedulerConfig = () => {
|
|||||||
{config.agingEnabled && (
|
{config.agingEnabled && (
|
||||||
<div data-testid="aging-interval-field">
|
<div data-testid="aging-interval-field">
|
||||||
<label htmlFor="aging-interval" className={labelCls}>
|
<label htmlFor="aging-interval" className={labelCls}>
|
||||||
Intervalo de Aging (ticks)
|
Aging Interval (ticks)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="aging-interval"
|
id="aging-interval"
|
||||||
@@ -236,7 +325,7 @@ const SchedulerConfig = () => {
|
|||||||
value={config.agingInterval}
|
value={config.agingInterval}
|
||||||
onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })}
|
onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
aria-label="Intervalo de aging"
|
aria-label="Aging interval"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -250,6 +339,16 @@ const SchedulerConfig = () => {
|
|||||||
onChange={queues => setConfig({ mlqQueues: queues })}
|
onChange={queues => setConfig({ mlqQueues: queues })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* MLFQ Level Configuration */}
|
||||||
|
{algo === 'MLFQ' && (
|
||||||
|
<MlfqLevelsConfig
|
||||||
|
levels={config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS}
|
||||||
|
boostInterval={config.mlfqBoostInterval ?? 0}
|
||||||
|
onLevelsChange={levels => setConfig({ mlfqLevels: levels })}
|
||||||
|
onBoostChange={v => setConfig({ mlfqBoostInterval: v })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import SchedulerConfig from './SchedulerConfig';
|
|||||||
|
|
||||||
const SchedulerPanel = () => {
|
const SchedulerPanel = () => {
|
||||||
return (
|
return (
|
||||||
<section aria-label="Painel do Escalonador" className="space-y-3">
|
<section aria-label="Scheduler Panel" className="space-y-3">
|
||||||
<div className="flex items-center gap-2 text-slate-300">
|
<div className="flex items-center gap-2 text-slate-300">
|
||||||
<Settings size={18} className="text-emerald-400" />
|
<Settings size={18} className="text-emerald-400" />
|
||||||
<h2 className="font-semibold uppercase text-xs tracking-wider">Escalonador</h2>
|
<h2 className="font-semibold uppercase text-xs tracking-wider">Scheduler</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AlgorithmSelector />
|
<AlgorithmSelector />
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ describe('ControlBar', () => {
|
|||||||
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
|
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "Pronto" status when idle', () => {
|
it('shows "Ready" status when idle', () => {
|
||||||
render(<ControlBar />);
|
render(<ControlBar />);
|
||||||
expect(screen.getByText('Pronto')).toBeInTheDocument();
|
expect(screen.getByText('Ready')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Play button is disabled when no processes are loaded', () => {
|
it('Play button is disabled when no processes are loaded', () => {
|
||||||
|
|||||||
@@ -11,62 +11,62 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('ProcessForm', () => {
|
describe('ProcessForm', () => {
|
||||||
it('renders the "Novo Processo" form with default fields', () => {
|
it('renders the "New Process" form with default fields', () => {
|
||||||
render(<ProcessForm onClose={() => {}} />);
|
render(<ProcessForm onClose={() => {}} />);
|
||||||
expect(screen.getByTestId('process-form')).toBeInTheDocument();
|
expect(screen.getByTestId('process-form')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Novo Processo')).toBeInTheDocument();
|
expect(screen.getByText('New Process')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Nome do processo')).toBeInTheDocument();
|
expect(screen.getByLabelText('Process name')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Tempo de chegada')).toBeInTheDocument();
|
expect(screen.getByLabelText('Arrival time')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Prioridade do processo')).toBeInTheDocument();
|
expect(screen.getByLabelText('Process priority')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "Editar Processo" title when editingProcess is provided', () => {
|
it('shows "Edit Process" title when editingProcess is provided', () => {
|
||||||
const proc = {
|
const proc = {
|
||||||
id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff',
|
id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff',
|
||||||
bursts: [{ type: 'cpu' as const, duration: 4 }],
|
bursts: [{ type: 'cpu' as const, duration: 4 }],
|
||||||
};
|
};
|
||||||
render(<ProcessForm editingProcess={proc} onClose={() => {}} />);
|
render(<ProcessForm editingProcess={proc} onClose={() => {}} />);
|
||||||
expect(screen.getByText('Editar Processo')).toBeInTheDocument();
|
expect(screen.getByText('Edit Process')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows validation error when name is empty', async () => {
|
it('shows validation error when name is empty', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<ProcessForm onClose={() => {}} />);
|
render(<ProcessForm onClose={() => {}} />);
|
||||||
const nameInput = screen.getByLabelText('Nome do processo');
|
const nameInput = screen.getByLabelText('Process name');
|
||||||
await user.clear(nameInput);
|
await user.clear(nameInput);
|
||||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
const submitBtn = screen.getByRole('button', { name: /Create/i });
|
||||||
await user.click(submitBtn);
|
await user.click(submitBtn);
|
||||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument();
|
expect(screen.getByText(/Name is required/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows validation error when arrival time is negative', async () => {
|
it('shows validation error when arrival time is negative', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<ProcessForm onClose={() => {}} />);
|
render(<ProcessForm onClose={() => {}} />);
|
||||||
const nameInput = screen.getByLabelText('Nome do processo');
|
const nameInput = screen.getByLabelText('Process name');
|
||||||
await user.type(nameInput, 'P1');
|
await user.type(nameInput, 'P1');
|
||||||
const arrivalInput = screen.getByLabelText('Tempo de chegada');
|
const arrivalInput = screen.getByLabelText('Arrival time');
|
||||||
// fireEvent.change used to reliably set a negative number bypassing HTML min attribute
|
// fireEvent.change used to reliably set a negative number bypassing HTML min attribute
|
||||||
fireEvent.change(arrivalInput, { target: { value: '-1' } });
|
fireEvent.change(arrivalInput, { target: { value: '-1' } });
|
||||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
const submitBtn = screen.getByRole('button', { name: /Create/i });
|
||||||
await user.click(submitBtn);
|
await user.click(submitBtn);
|
||||||
expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument();
|
expect(screen.getByText(/Arrival Time must be ≥ 0/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows validation error for name longer than 8 chars', async () => {
|
it('shows validation error for name longer than 8 chars', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<ProcessForm onClose={() => {}} />);
|
render(<ProcessForm onClose={() => {}} />);
|
||||||
const nameInput = screen.getByLabelText('Nome do processo');
|
const nameInput = screen.getByLabelText('Process name');
|
||||||
// Bypass the HTML maxLength attribute via fireEvent to test the JS validation
|
// Bypass the HTML maxLength attribute via fireEvent to test the JS validation
|
||||||
fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } });
|
fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } });
|
||||||
await user.click(screen.getByRole('button', { name: /Criar/i }));
|
await user.click(screen.getByRole('button', { name: /Create/i }));
|
||||||
expect(screen.getByText(/Nome deve ter no máximo 8 caracteres/i)).toBeInTheDocument();
|
expect(screen.getByText(/Name must be at most 8 characters/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a new burst with alternating type when clicking + button', async () => {
|
it('adds a new burst with alternating type when clicking + button', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<ProcessForm onClose={() => {}} />);
|
render(<ProcessForm onClose={() => {}} />);
|
||||||
const addBurstBtn = screen.getByLabelText('Adicionar burst');
|
const addBurstBtn = screen.getByLabelText('Add burst');
|
||||||
await user.click(addBurstBtn);
|
await user.click(addBurstBtn);
|
||||||
// After one CPU burst at start, clicking + adds IO burst
|
// After one CPU burst at start, clicking + adds IO burst
|
||||||
// getAllByText with exact matches for the type labels
|
// getAllByText with exact matches for the type labels
|
||||||
@@ -80,9 +80,9 @@ describe('ProcessForm', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
render(<ProcessForm onClose={onClose} />);
|
render(<ProcessForm onClose={onClose} />);
|
||||||
const nameInput = screen.getByLabelText('Nome do processo');
|
const nameInput = screen.getByLabelText('Process name');
|
||||||
await user.type(nameInput, 'P1');
|
await user.type(nameInput, 'P1');
|
||||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
const submitBtn = screen.getByRole('button', { name: /Create/i });
|
||||||
await user.click(submitBtn);
|
await user.click(submitBtn);
|
||||||
expect(onClose).toHaveBeenCalledOnce();
|
expect(onClose).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
@@ -90,9 +90,9 @@ describe('ProcessForm', () => {
|
|||||||
it('adds the process to the store after successful submission', async () => {
|
it('adds the process to the store after successful submission', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<ProcessForm onClose={() => {}} />);
|
render(<ProcessForm onClose={() => {}} />);
|
||||||
const nameInput = screen.getByLabelText('Nome do processo');
|
const nameInput = screen.getByLabelText('Process name');
|
||||||
await user.type(nameInput, 'Test1');
|
await user.type(nameInput, 'Test1');
|
||||||
await user.click(screen.getByRole('button', { name: /Criar/i }));
|
await user.click(screen.getByRole('button', { name: /Create/i }));
|
||||||
const processes = useProcessStore.getState().processes;
|
const processes = useProcessStore.getState().processes;
|
||||||
expect(processes).toHaveLength(1);
|
expect(processes).toHaveLength(1);
|
||||||
expect(processes[0]?.name).toBe('Test1');
|
expect(processes[0]?.name).toBe('Test1');
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ beforeEach(() => {
|
|||||||
describe('SchedulerConfig', () => {
|
describe('SchedulerConfig', () => {
|
||||||
it('always shows Context Switch field regardless of algorithm', () => {
|
it('always shows Context Switch field regardless of algorithm', () => {
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument();
|
expect(screen.getByLabelText('Context switch time')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('always shows read-only preemptive badge', () => {
|
it('always shows read-only preemptive badge', () => {
|
||||||
@@ -34,23 +34,23 @@ describe('SchedulerConfig', () => {
|
|||||||
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
|
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('badge shows "Não-preemptivo" for FCFS', () => {
|
it('badge shows "Non-preemptive" for FCFS', () => {
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
|
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('badge shows "Preemptivo" for SJF_P (SRTF)', () => {
|
it('badge shows "Preemptive" for SJF_P (SRTF)', () => {
|
||||||
setAlgorithm('SJF_P');
|
setAlgorithm('SJF_P');
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo');
|
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptive');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => {
|
it('badge shows "Non-preemptive" for SJF_NP (preemptive toggle no longer exists)', () => {
|
||||||
setAlgorithm('SJF_NP');
|
setAlgorithm('SJF_NP');
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
|
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
|
||||||
// Confirm the old interactive preemptive switch is gone
|
// Confirm the old interactive preemptive switch is gone
|
||||||
expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('switch', { name: /Preemptive/i })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does NOT show Quantum field for FCFS', () => {
|
it('does NOT show Quantum field for FCFS', () => {
|
||||||
@@ -62,7 +62,7 @@ describe('SchedulerConfig', () => {
|
|||||||
setAlgorithm('RR');
|
setAlgorithm('RR');
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
|
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
|
expect(screen.getByLabelText('Round Robin quantum')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => {
|
it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => {
|
||||||
@@ -97,13 +97,13 @@ describe('SchedulerConfig', () => {
|
|||||||
const agingToggle = screen.getByRole('switch', { name: /Aging/i });
|
const agingToggle = screen.getByRole('switch', { name: /Aging/i });
|
||||||
await user.click(agingToggle);
|
await user.click(agingToggle);
|
||||||
expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument();
|
expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Intervalo de aging')).toBeInTheDocument();
|
expect(screen.getByLabelText('Aging interval')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the store when Quantum field is changed', () => {
|
it('updates the store when Quantum field is changed', () => {
|
||||||
setAlgorithm('RR');
|
setAlgorithm('RR');
|
||||||
render(<SchedulerConfig />);
|
render(<SchedulerConfig />);
|
||||||
const quantumInput = screen.getByLabelText('Quantum do Round Robin');
|
const quantumInput = screen.getByLabelText('Round Robin quantum');
|
||||||
fireEvent.change(quantumInput, { target: { value: '5' } });
|
fireEvent.change(quantumInput, { target: { value: '5' } });
|
||||||
expect(useProcessStore.getState().config.quantum).toBe(5);
|
expect(useProcessStore.getState().config.quantum).toBe(5);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Deriva uma cor para uma thread a partir da cor base do processo.
|
||||||
|
* Desloca o matiz (hue) em 35° por índice de thread para garantir
|
||||||
|
* cores visualmente distintas dentro do mesmo processo.
|
||||||
|
*/
|
||||||
|
export function deriveThreadColor(processColor: string, threadIndex: number): string {
|
||||||
|
const [h, s, l] = hexToHsl(processColor);
|
||||||
|
const newH = (h + threadIndex * 40) % 360;
|
||||||
|
const newL = Math.min(80, Math.max(35, l + (threadIndex % 2 === 0 ? 8 : -8)));
|
||||||
|
return hslToHex(newH, Math.min(100, s + 10), newL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToHsl(hex: string): [number, number, number] {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
let h = 0;
|
||||||
|
let s = 0;
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
|
||||||
|
if (max !== min) {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||||
|
case g: h = ((b - r) / d + 2) / 6; break;
|
||||||
|
case b: h = ((r - g) / d + 4) / 6; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [h * 360, s * 100, l * 100];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslToHex(h: number, s: number, l: number): string {
|
||||||
|
h /= 360; s /= 100; l /= 100;
|
||||||
|
let r: number, g: number, b: number;
|
||||||
|
|
||||||
|
if (s === 0) {
|
||||||
|
r = g = b = l;
|
||||||
|
} else {
|
||||||
|
const hue2rgb = (p: number, q: number, t: number) => {
|
||||||
|
if (t < 0) t += 1;
|
||||||
|
if (t > 1) t -= 1;
|
||||||
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||||
|
if (t < 1 / 2) return q;
|
||||||
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hue2rgb(p, q, h + 1 / 3);
|
||||||
|
g = hue2rgb(p, q, h);
|
||||||
|
b = hue2rgb(p, q, h - 1 / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toHex = (x: number) =>
|
||||||
|
Math.round(x * 255).toString(16).padStart(2, "0");
|
||||||
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,690 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { SimulationEngine } from "../engine";
|
||||||
|
import type { Process, SchedulerConfig } from "../types";
|
||||||
|
|
||||||
|
const BASE_CONFIG: SchedulerConfig = {
|
||||||
|
algorithm: "FCFS",
|
||||||
|
quantum: 10,
|
||||||
|
contextSwitchTime: 0,
|
||||||
|
isPreemptive: false,
|
||||||
|
agingEnabled: false,
|
||||||
|
agingInterval: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeThreadedProcess(
|
||||||
|
overrides: Partial<Process> = {},
|
||||||
|
): Process {
|
||||||
|
return {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "P1",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t2",
|
||||||
|
name: "T2",
|
||||||
|
bursts: [{ type: "cpu", duration: 2 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ONE_TO_ONE ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — ONE_TO_ONE básico", () => {
|
||||||
|
it("processo termina quando todas as threads terminam", () => {
|
||||||
|
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
const rt = engine.runtimeStates[0]!;
|
||||||
|
expect(rt.state).toBe("Terminated");
|
||||||
|
expect(rt.finishTick).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runtimes de threads são gerados corretamente", () => {
|
||||||
|
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
const threadRts = engine.threadRuntimeStates;
|
||||||
|
expect(threadRts).toHaveLength(2);
|
||||||
|
for (const trt of threadRts) {
|
||||||
|
expect(trt.state).toBe("Terminated");
|
||||||
|
expect(trt.cpuTime).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cpuTime total do processo = soma dos cpuTimes das threads", () => {
|
||||||
|
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
const pRt = engine.runtimeStates[0]!;
|
||||||
|
const threadTotal = engine.threadRuntimeStates.reduce(
|
||||||
|
(sum, trt) => sum + trt.cpuTime,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(pRt.cpuTime).toBe(threadTotal);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processHasThreads retorna true para processo com threads", () => {
|
||||||
|
const p = makeThreadedProcess();
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
expect(engine.processHasThreads("p1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processo sem threads não é afetado", () => {
|
||||||
|
const p: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "P1",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#aaa",
|
||||||
|
bursts: [{ type: "cpu", duration: 4 }],
|
||||||
|
};
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
expect(engine.processHasThreads("p1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ONE_TO_ONE: processo com thread em I/O pode ter outra thread rodando", () => {
|
||||||
|
const p = makeThreadedProcess({
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t2",
|
||||||
|
name: "T2",
|
||||||
|
bursts: [{ type: "cpu", duration: 4 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
|
||||||
|
// O processo NÃO deve ir para Waiting enquanto T2 está ready/running
|
||||||
|
const waitingTicks = ticks.filter(
|
||||||
|
(tick) => tick.states["p1"] === "Waiting",
|
||||||
|
);
|
||||||
|
// Pode ter algum tick Waiting quando AMBAS as threads estão em I/O,
|
||||||
|
// mas aqui T2 não tem I/O, então o processo nunca deve ficar Waiting
|
||||||
|
expect(waitingTicks.length).toBe(0);
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ONE_TO_ONE: processo vai para Waiting apenas quando todas as threads estão em I/O", () => {
|
||||||
|
const p = makeThreadedProcess({
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t2",
|
||||||
|
name: "T2",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
|
||||||
|
// Deve haver ticks em que o processo está Waiting (ambas as threads em I/O)
|
||||||
|
const waitingTicks = ticks.filter(
|
||||||
|
(tick) => tick.states["p1"] === "Waiting",
|
||||||
|
);
|
||||||
|
expect(waitingTicks.length).toBeGreaterThan(0);
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── MANY_TO_ONE ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — MANY_TO_ONE", () => {
|
||||||
|
it("processo vai para Waiting quando qualquer thread está em I/O", () => {
|
||||||
|
const p = makeThreadedProcess({
|
||||||
|
threadModel: "MANY_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t2",
|
||||||
|
name: "T2",
|
||||||
|
// Esta thread ficaria pronta enquanto T1 está em I/O,
|
||||||
|
// mas em MANY_TO_ONE o processo bloqueia inteiro
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
|
||||||
|
// Deve haver ticks em Waiting (T1 em I/O bloqueia o processo)
|
||||||
|
const waitingTicks = ticks.filter(
|
||||||
|
(tick) => tick.states["p1"] === "Waiting",
|
||||||
|
);
|
||||||
|
expect(waitingTicks.length).toBeGreaterThan(0);
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processo termina quando todas as threads terminam", () => {
|
||||||
|
const p = makeThreadedProcess({ threadModel: "MANY_TO_ONE" });
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
expect(engine.runtimeStates[0]!.state).toBe("Terminated");
|
||||||
|
const allTerminated = engine.threadRuntimeStates.every(
|
||||||
|
(trt) => trt.state === "Terminated",
|
||||||
|
);
|
||||||
|
expect(allTerminated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("MANY_TO_ONE tem mais ticks em Waiting que ONE_TO_ONE para mesmo workload", () => {
|
||||||
|
const threads = [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu" as const, duration: 2 },
|
||||||
|
{ type: "io" as const, duration: 4 },
|
||||||
|
{ type: "cpu" as const, duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t2",
|
||||||
|
name: "T2",
|
||||||
|
bursts: [{ type: "cpu" as const, duration: 3 }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const m2o = makeThreadedProcess({ threadModel: "MANY_TO_ONE", threads });
|
||||||
|
const o2o = makeThreadedProcess({ threadModel: "ONE_TO_ONE", threads });
|
||||||
|
|
||||||
|
const engine1 = new SimulationEngine([m2o], BASE_CONFIG);
|
||||||
|
const ticks1 = engine1.runAll();
|
||||||
|
|
||||||
|
const engine2 = new SimulationEngine([o2o], BASE_CONFIG);
|
||||||
|
const ticks2 = engine2.runAll();
|
||||||
|
|
||||||
|
const waitingM2O = ticks1.filter(
|
||||||
|
(t) => t.states["p1"] === "Waiting",
|
||||||
|
).length;
|
||||||
|
const waitingO2O = ticks2.filter(
|
||||||
|
(t) => t.states["p1"] === "Waiting",
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// MANY_TO_ONE bloqueia o processo durante I/O da T1 mesmo com T2 pronta
|
||||||
|
expect(waitingM2O).toBeGreaterThanOrEqual(waitingO2O);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── MANY_TO_MANY ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — MANY_TO_MANY", () => {
|
||||||
|
it("respeita limite de kernel threads (M=2 para 3 user threads)", () => {
|
||||||
|
const p: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "App",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#CE93D8",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "MANY_TO_MANY",
|
||||||
|
kernelThreadCount: 2,
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 4 }] },
|
||||||
|
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
|
{ tid: "t3", name: "T3", bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
// Todos os threads devem ter terminado
|
||||||
|
const allTerminated = engine.threadRuntimeStates.every(
|
||||||
|
(trt) => trt.state === "Terminated",
|
||||||
|
);
|
||||||
|
expect(allTerminated).toBe(true);
|
||||||
|
// Deve haver kernel wait info em algum tick
|
||||||
|
const hasKernelWait = ticks.some(
|
||||||
|
(tick) =>
|
||||||
|
tick.kernelWaitingThreads !== undefined &&
|
||||||
|
Object.keys(tick.kernelWaitingThreads).length > 0,
|
||||||
|
);
|
||||||
|
expect(hasKernelWait).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("termina corretamente com M=N (todos têm kernel thread)", () => {
|
||||||
|
const p: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "App",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#CE93D8",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "MANY_TO_MANY",
|
||||||
|
kernelThreadCount: 3, // igual ao número de threads
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
|
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
|
{ tid: "t3", name: "T3", bursts: [{ type: "cpu", duration: 1 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Snapshot de threads ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — snapshot com threads", () => {
|
||||||
|
it("threadStates presentes no snapshot quando processo tem threads", () => {
|
||||||
|
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const tick = engine.step();
|
||||||
|
expect(tick.threadStates).toBeDefined();
|
||||||
|
expect(tick.threadStates!["p1"]).toBeDefined();
|
||||||
|
expect(Object.keys(tick.threadStates!["p1"]!)).toContain("t1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cpuThreads indica thread ativa no processo", () => {
|
||||||
|
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
engine.step(); // tick 0 — despacha processo e thread
|
||||||
|
const tick = engine.step(); // tick 1 — thread em execução
|
||||||
|
expect(tick.cpuThreads).toBeDefined();
|
||||||
|
expect(tick.cpuThreads!["p1"]).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Múltiplos processos com e sem threads ─────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — mix de processos com e sem threads", () => {
|
||||||
|
it("processa corretamente processos com e sem threads simultaneamente", () => {
|
||||||
|
const pThreaded: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "Threaded",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threads: [
|
||||||
|
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const pSimple: Process = {
|
||||||
|
pid: 2,
|
||||||
|
id: "p2",
|
||||||
|
name: "Simple",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#81C784",
|
||||||
|
bursts: [{ type: "cpu", duration: 4 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([pThreaded, pSimple], BASE_CONFIG);
|
||||||
|
engine.runAll();
|
||||||
|
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
const rts = engine.runtimeStates;
|
||||||
|
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Política intra-processo RR ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — RR intra-processo", () => {
|
||||||
|
it("rotaciona threads dentro do processo com quantum 2", () => {
|
||||||
|
const p = makeThreadedProcess({
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "RR",
|
||||||
|
threadQuantum: 2,
|
||||||
|
threads: [
|
||||||
|
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 6 }] },
|
||||||
|
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 4 }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
|
||||||
|
// Verifica que ambas as threads ficaram ativas em ticks diferentes
|
||||||
|
const t1Active = ticks.filter(
|
||||||
|
(tick) => tick.cpuThreads?.["p1"] === "t1",
|
||||||
|
).length;
|
||||||
|
const t2Active = ticks.filter(
|
||||||
|
(tick) => tick.cpuThreads?.["p1"] === "t2",
|
||||||
|
).length;
|
||||||
|
|
||||||
|
expect(t1Active).toBeGreaterThan(0);
|
||||||
|
expect(t2Active).toBeGreaterThan(0);
|
||||||
|
expect(engine.isFinished).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Quantum de processo com threads ────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Testa o comportamento correto quando o burst de uma thread termina
|
||||||
|
// exatamente no mesmo tick em que o quantum do processo expira.
|
||||||
|
// Antes do fix, o processo recebia quantum+1 ticks (bug).
|
||||||
|
|
||||||
|
describe("Engine — quantum de processo com threads (regressão)", () => {
|
||||||
|
const rrConfig: SchedulerConfig = {
|
||||||
|
algorithm: "RR",
|
||||||
|
quantum: 2,
|
||||||
|
contextSwitchTime: 0,
|
||||||
|
isPreemptive: false,
|
||||||
|
agingEnabled: false,
|
||||||
|
agingInterval: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityRrConfig: SchedulerConfig = {
|
||||||
|
algorithm: "PRIORITY_RR",
|
||||||
|
quantum: 2,
|
||||||
|
contextSwitchTime: 0,
|
||||||
|
isPreemptive: false,
|
||||||
|
agingEnabled: false,
|
||||||
|
agingInterval: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("RR: segundo processo recebe CPU no tick 2 quando burst da thread = quantum", () => {
|
||||||
|
// P1: thread T1 com CPU=2 (= quantum). Ao fim do tick 1, T1 termina E
|
||||||
|
// o quantum expira ao mesmo tempo. P1 deve ceder a CPU imediatamente.
|
||||||
|
// P2 deve começar no tick 2, não no tick 3.
|
||||||
|
const p1: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "P1",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
|
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const p2: Process = {
|
||||||
|
pid: 2,
|
||||||
|
id: "p2",
|
||||||
|
name: "P2",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#81C784",
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
// Tick 0 e 1: P1 roda (T1, burst=2=quantum)
|
||||||
|
expect(cpu[0]).toBe("p1");
|
||||||
|
expect(cpu[1]).toBe("p1");
|
||||||
|
// Tick 2: P2 deve receber CPU (não tick 3)
|
||||||
|
expect(cpu[2]).toBe("p2");
|
||||||
|
|
||||||
|
const rt2 = engine.runtimeStates.find((r) => r.processId === "p2")!;
|
||||||
|
expect(rt2.startTick).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("RR: processo alterna exatamente a cada quantum quando bursts coincidem com quantum", () => {
|
||||||
|
// Ambos os processos têm threads com CPU=2 = quantum.
|
||||||
|
// Devem alternar estritamente: P1(0-1), P2(2-3), P1(4-5), P2(6-7)...
|
||||||
|
const makeThreaded = (id: string, pid: number, color: string): Process => ({
|
||||||
|
pid,
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color,
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE" as const,
|
||||||
|
threadSchedulingPolicy: "FCFS" as const,
|
||||||
|
threads: [
|
||||||
|
{ tid: `${id}_t1`, name: "T1", bursts: [{ type: "cpu" as const, duration: 2 }] },
|
||||||
|
{ tid: `${id}_t2`, name: "T2", bursts: [{ type: "cpu" as const, duration: 2 }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const engine = new SimulationEngine(
|
||||||
|
[makeThreaded("p1", 1, "#4FC3F7"), makeThreaded("p2", 2, "#81C784")],
|
||||||
|
rrConfig,
|
||||||
|
);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
// Cada bloco de 2 ticks deve pertencer a um processo diferente
|
||||||
|
expect(cpu[0]).toBe("p1");
|
||||||
|
expect(cpu[1]).toBe("p1");
|
||||||
|
expect(cpu[2]).toBe("p2");
|
||||||
|
expect(cpu[3]).toBe("p2");
|
||||||
|
expect(cpu[4]).toBe("p1");
|
||||||
|
expect(cpu[5]).toBe("p1");
|
||||||
|
expect(cpu[6]).toBe("p2");
|
||||||
|
expect(cpu[7]).toBe("p2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PRIORITY_RR: reproduz o cenário reportado — P2.startTick deve ser 2", () => {
|
||||||
|
// Replica o config.json do usuário (versão simplificada):
|
||||||
|
// App: ONE_TO_ONE, PRIORITY, High-1(prio=1, cpu=2), High-2(prio=2, cpu=2)
|
||||||
|
// P2: ONE_TO_ONE, FCFS, T1(cpu=3)
|
||||||
|
// Algoritmo: PRIORITY_RR, quantum=2, ambos prio=1
|
||||||
|
// Bug: App rodava ticks 0,1,2 (quantum+1). Correto: 0,1 → P2 começa no tick 2.
|
||||||
|
const app: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "app",
|
||||||
|
name: "App",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 1,
|
||||||
|
color: "#F06292",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "PRIORITY",
|
||||||
|
threads: [
|
||||||
|
{ tid: "h1", name: "High-1", priority: 1, bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
|
{ tid: "h2", name: "High-2", priority: 2, bursts: [{ type: "cpu", duration: 2 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const p2: Process = {
|
||||||
|
pid: 2,
|
||||||
|
id: "p2",
|
||||||
|
name: "P2",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 1,
|
||||||
|
color: "#f472b6",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{ tid: "p2t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([app, p2], priorityRrConfig);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
// App roda ticks 0 e 1 (quantum=2). P2 deve começar no tick 2.
|
||||||
|
expect(cpu[0]).toBe("app");
|
||||||
|
expect(cpu[1]).toBe("app");
|
||||||
|
expect(cpu[2]).toBe("p2");
|
||||||
|
|
||||||
|
const p2Rt = engine.runtimeStates.find((r) => r.processId === "p2")!;
|
||||||
|
expect(p2Rt.startTick).toBe(2);
|
||||||
|
// responseTime de threads é calculado em _dispatchThreadForProcess
|
||||||
|
const p2ThreadRts = engine.threadRuntimeStates.filter((t) => t.processId === "p2");
|
||||||
|
expect(p2ThreadRts[0]!.startTick).toBe(2);
|
||||||
|
expect(p2ThreadRts[0]!.responseTime).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thread com burst menor que quantum não causa preempção prematura", () => {
|
||||||
|
// T1 tem burst=1 (< quantum=2). T1 termina no tick 1 sem quantum expirado.
|
||||||
|
// T2 deve continuar rodando no tick 2 (dentro do mesmo quantum).
|
||||||
|
// P2 só recebe CPU no tick 3 (quando quantum do processo expirar com T2).
|
||||||
|
const p1: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "P1",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 1 }] },
|
||||||
|
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const p2: Process = {
|
||||||
|
pid: 2,
|
||||||
|
id: "p2",
|
||||||
|
name: "P2",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#81C784",
|
||||||
|
bursts: [{ type: "cpu", duration: 2 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
// Tick 0: P1/T1 roda (burst=1, quantum=2 → quantum não expirado)
|
||||||
|
expect(cpu[0]).toBe("p1");
|
||||||
|
// Tick 1: P1/T2 assume (quantum restante=1)
|
||||||
|
expect(cpu[1]).toBe("p1");
|
||||||
|
// Tick 2: quantum do P1 expira → P2 recebe CPU
|
||||||
|
expect(cpu[2]).toBe("p2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processo com thread em I/O no momento do quantum cede corretamente", () => {
|
||||||
|
// T1: CPU(2) → IO(2) → CPU(1). Quantum=2.
|
||||||
|
// T1 termina burst CPU no tick 1 E quantum=0: vai para IO, processo cede.
|
||||||
|
// P2 deve receber CPU no tick 2.
|
||||||
|
const p1: Process = {
|
||||||
|
pid: 1,
|
||||||
|
id: "p1",
|
||||||
|
name: "P1",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 2 },
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const p2: Process = {
|
||||||
|
pid: 2,
|
||||||
|
id: "p2",
|
||||||
|
name: "P2",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#81C784",
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
expect(cpu[0]).toBe("p1");
|
||||||
|
expect(cpu[1]).toBe("p1");
|
||||||
|
// Tick 2: T1 foi para IO e quantum expirou → P2 recebe CPU
|
||||||
|
expect(cpu[2]).toBe("p2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Política intra-processo PRIORITY ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe("Engine — Priority intra-processo", () => {
|
||||||
|
it("thread de maior prioridade roda primeiro (menor número = maior prio)", () => {
|
||||||
|
const p = makeThreadedProcess({
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "PRIORITY",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t1",
|
||||||
|
name: "Low",
|
||||||
|
priority: 8,
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t2",
|
||||||
|
name: "High",
|
||||||
|
priority: 1,
|
||||||
|
bursts: [{ type: "cpu", duration: 2 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||||
|
const firstTick = engine.step();
|
||||||
|
|
||||||
|
// t2 (prioridade 1) deve ser selecionada primeiro
|
||||||
|
expect(firstTick.cpuThreads?.["p1"]).toBe("t2");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -236,11 +236,11 @@ describe("SimulationEngine — ciclo de vida", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SimulationEngine — chegada de processos
|
// SimulationEngine — process arrival
|
||||||
// Tests: late arrivals, idle ticks while waiting for process
|
// Tests: late arrivals, idle ticks while waiting for process
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
describe("SimulationEngine — chegada de processos", () => {
|
describe("SimulationEngine — process arrival", () => {
|
||||||
it("process arriving after simulation starts is correctly scheduled", () => {
|
it("process arriving after simulation starts is correctly scheduled", () => {
|
||||||
const engine = new SimulationEngine(
|
const engine = new SimulationEngine(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { MLFQAlgorithm } from "../algorithms/mlfq";
|
||||||
|
import { SimulationEngine } from "../engine";
|
||||||
|
import type { MlfqLevelDef, Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||||
|
|
||||||
|
const mlfq = new MLFQAlgorithm();
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
const DEFAULT_LEVELS: MlfqLevelDef[] = [
|
||||||
|
{ quantum: 2 },
|
||||||
|
{ quantum: 4 },
|
||||||
|
{ quantum: 8 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function makeRt(
|
||||||
|
id: string,
|
||||||
|
opts: {
|
||||||
|
arrivalTick?: number;
|
||||||
|
quantumRemaining?: number;
|
||||||
|
mlfqLevel?: number;
|
||||||
|
waitingTime?: number;
|
||||||
|
remainingBurst?: number;
|
||||||
|
} = {},
|
||||||
|
): ProcessRuntime {
|
||||||
|
return {
|
||||||
|
processId: id,
|
||||||
|
state: "Ready",
|
||||||
|
remainingBurst: opts.remainingBurst ?? 10,
|
||||||
|
burstIndex: 0,
|
||||||
|
arrivalTick: opts.arrivalTick ?? 0,
|
||||||
|
startTick: null,
|
||||||
|
finishTick: null,
|
||||||
|
waitingTime: opts.waitingTime ?? 0,
|
||||||
|
cpuTime: 0,
|
||||||
|
ioTime: 0,
|
||||||
|
responseTime: null,
|
||||||
|
quantumRemaining: opts.quantumRemaining ?? 2,
|
||||||
|
currentPriority: 5,
|
||||||
|
mlfqLevel: opts.mlfqLevel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
|
||||||
|
algorithm: "MLFQ",
|
||||||
|
quantum: 2,
|
||||||
|
contextSwitchTime: 0,
|
||||||
|
isPreemptive: false,
|
||||||
|
agingEnabled: false,
|
||||||
|
agingInterval: 5,
|
||||||
|
mlfqLevels: DEFAULT_LEVELS,
|
||||||
|
mlfqBoostInterval: 0,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const p = (
|
||||||
|
id: string,
|
||||||
|
arrival: number,
|
||||||
|
cpuDuration: number,
|
||||||
|
): Process => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
arrivalTime: arrival,
|
||||||
|
priority: 5,
|
||||||
|
color: "#fff",
|
||||||
|
bursts: [{ type: "cpu", duration: cpuDuration }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pWithIo = (
|
||||||
|
id: string,
|
||||||
|
arrival: number,
|
||||||
|
cpu1: number,
|
||||||
|
io: number,
|
||||||
|
cpu2: number,
|
||||||
|
): Process => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
arrivalTime: arrival,
|
||||||
|
priority: 5,
|
||||||
|
color: "#fff",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: cpu1 },
|
||||||
|
{ type: "io", duration: io },
|
||||||
|
{ type: "cpu", duration: cpu2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: select() — initialization
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ select() — initialization", () => {
|
||||||
|
it("returns null for empty queue", () => {
|
||||||
|
expect(mlfq.select([], cfg())).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assigns level 0 to a new process (mlfqLevel undefined)", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: undefined });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.mlfqLevel).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not re-initialize a process that already has mlfqLevel 0", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 2 });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.mlfqLevel).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: select() — demotion
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ select() — demotion", () => {
|
||||||
|
it("demotes a quantum-expired process (quantumRemaining <= 0) from level 0 to 1", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 0 });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.mlfqLevel).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("demotes from level 1 to level 2", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 1, quantumRemaining: 0 });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.mlfqLevel).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps demotion at the last level (no demotion beyond level N-1)", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 2, quantumRemaining: 0 });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.mlfqLevel).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets quantumRemaining sentinel to 1 after demotion", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 0 });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.quantumRemaining).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT demote a process returning from I/O (quantumRemaining > 0)", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 1 });
|
||||||
|
mlfq.select([rt], cfg());
|
||||||
|
expect(rt.mlfqLevel).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: select() — priority selection within/across levels
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ select() — level-based selection", () => {
|
||||||
|
it("selects the process at the highest-priority level (lowest index)", () => {
|
||||||
|
const low = makeRt("lo", { mlfqLevel: 2, quantumRemaining: 2 });
|
||||||
|
const mid = makeRt("mid", { mlfqLevel: 1, quantumRemaining: 2 });
|
||||||
|
const high = makeRt("hi", { mlfqLevel: 0, quantumRemaining: 2 });
|
||||||
|
expect(mlfq.select([low, mid, high], cfg())?.processId).toBe("hi");
|
||||||
|
expect(mlfq.select([mid, low], cfg())?.processId).toBe("mid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses FIFO (arrivalTick) within the same level", () => {
|
||||||
|
const a = makeRt("a", { mlfqLevel: 0, arrivalTick: 5, quantumRemaining: 2 });
|
||||||
|
const b = makeRt("b", { mlfqLevel: 0, arrivalTick: 2, quantumRemaining: 2 });
|
||||||
|
const c = makeRt("c", { mlfqLevel: 0, arrivalTick: 8, quantumRemaining: 2 });
|
||||||
|
expect(mlfq.select([a, b, c], cfg())?.processId).toBe("b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses processId as tiebreak when arrivalTick is equal", () => {
|
||||||
|
const a = makeRt("z", { mlfqLevel: 1, arrivalTick: 3, quantumRemaining: 2 });
|
||||||
|
const b = makeRt("a", { mlfqLevel: 1, arrivalTick: 3, quantumRemaining: 2 });
|
||||||
|
expect(mlfq.select([a, b], cfg())?.processId).toBe("a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: select() — priority boost
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ select() — priority boost", () => {
|
||||||
|
it("does NOT boost when boostInterval is 0 (disabled)", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 100, quantumRemaining: 2 });
|
||||||
|
mlfq.select([rt], cfg({ mlfqBoostInterval: 0 }));
|
||||||
|
expect(rt.mlfqLevel).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("boosts a process to level 0 when waitingTime >= boostInterval", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 10, quantumRemaining: 2 });
|
||||||
|
mlfq.select([rt], cfg({ mlfqBoostInterval: 10 }));
|
||||||
|
expect(rt.mlfqLevel).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT boost a process that hasn't waited long enough", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 9, quantumRemaining: 2 });
|
||||||
|
mlfq.select([rt], cfg({ mlfqBoostInterval: 10 }));
|
||||||
|
expect(rt.mlfqLevel).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT boost a process already at level 0", () => {
|
||||||
|
const rt = makeRt("p1", { mlfqLevel: 0, waitingTime: 100, quantumRemaining: 2 });
|
||||||
|
mlfq.select([rt], cfg({ mlfqBoostInterval: 5 }));
|
||||||
|
expect(rt.mlfqLevel).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: shouldPreempt()
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ shouldPreempt()", () => {
|
||||||
|
it("preempts when candidate is at a higher-priority level (lower index)", () => {
|
||||||
|
const running = makeRt("r", { mlfqLevel: 2 });
|
||||||
|
const candidate = makeRt("c", { mlfqLevel: 0 });
|
||||||
|
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preempts from level 1 by level 0", () => {
|
||||||
|
const running = makeRt("r", { mlfqLevel: 1 });
|
||||||
|
const candidate = makeRt("c", { mlfqLevel: 0 });
|
||||||
|
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT preempt when candidate is at the same level", () => {
|
||||||
|
const running = makeRt("r", { mlfqLevel: 1 });
|
||||||
|
const candidate = makeRt("c", { mlfqLevel: 1 });
|
||||||
|
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT preempt when candidate is at a lower-priority level", () => {
|
||||||
|
const running = makeRt("r", { mlfqLevel: 0 });
|
||||||
|
const candidate = makeRt("c", { mlfqLevel: 2 });
|
||||||
|
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: isQuantumExpired()
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ isQuantumExpired()", () => {
|
||||||
|
it("returns true when quantumRemaining <= 0", () => {
|
||||||
|
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: 0 }), cfg())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when quantumRemaining is negative", () => {
|
||||||
|
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: -1 }), cfg())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when quantumRemaining > 0", () => {
|
||||||
|
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: 1 }), cfg())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Unit: getQuantumForProcess()
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ getQuantumForProcess()", () => {
|
||||||
|
it("returns the quantum for level 0 (default: 2)", () => {
|
||||||
|
const rt = makeRt("p", { mlfqLevel: 0 });
|
||||||
|
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the quantum for level 1 (default: 4)", () => {
|
||||||
|
const rt = makeRt("p", { mlfqLevel: 1 });
|
||||||
|
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the quantum for level 2 (default: 8)", () => {
|
||||||
|
const rt = makeRt("p", { mlfqLevel: 2 });
|
||||||
|
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to config.quantum when mlfqLevel is undefined", () => {
|
||||||
|
const rt = makeRt("p", { mlfqLevel: undefined });
|
||||||
|
expect(mlfq.getQuantumForProcess(rt, cfg({ quantum: 3 }))).toBe(2); // level 0 quantum
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects custom level quantums", () => {
|
||||||
|
const custom = cfg({ mlfqLevels: [{ quantum: 1 }, { quantum: 3 }, { quantum: 7 }] });
|
||||||
|
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 0 }), custom)).toBe(1);
|
||||||
|
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 1 }), custom)).toBe(3);
|
||||||
|
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 2 }), custom)).toBe(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Integration — CT-MLFQ-01: single process demotion sequence
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ integration — CT-MLFQ-01: demotion sequence", () => {
|
||||||
|
it("single process is demoted through levels, with per-level quantum (CT-MLFQ-01)", () => {
|
||||||
|
// P1: CPU burst of 14 ticks
|
||||||
|
// Level 0: quantum=2 → runs ticks 0-1, demoted to level 1
|
||||||
|
// Level 1: quantum=4 → runs ticks 2-5, demoted to level 2
|
||||||
|
// Level 2: quantum=8 → runs ticks 6-13 (burst completes)
|
||||||
|
const processes = [p("p1", 0, 14)];
|
||||||
|
const engine = new SimulationEngine(processes, cfg());
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
expect(cpu).toHaveLength(14);
|
||||||
|
expect(cpu.every((id) => id === "p1")).toBe(true);
|
||||||
|
// Verify total cpuTime
|
||||||
|
const rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
|
||||||
|
expect(rt.cpuTime).toBe(14);
|
||||||
|
expect(rt.state).toBe("Terminated");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Integration — CT-MLFQ-02: new arrival preempts demoted process
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ integration — CT-MLFQ-02: preemption by new process", () => {
|
||||||
|
it("a newly arrived process (level 0) preempts a level-1 running process (CT-MLFQ-02)", () => {
|
||||||
|
// P1: arrives 0, CPU:10 — will be at level 1 after tick 2
|
||||||
|
// P2: arrives 3, CPU:2 — enters at level 0, should preempt P1 at tick 3
|
||||||
|
const processes = [p("p1", 0, 10), p("p2", 3, 2)];
|
||||||
|
const engine = new SimulationEngine(processes, cfg());
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
// P1 runs ticks 0-1 (quantum=2, level 0)
|
||||||
|
expect(cpu[0]).toBe("p1");
|
||||||
|
expect(cpu[1]).toBe("p1");
|
||||||
|
// P1 is demoted to level 1 at tick 2, starts running with quantum=4
|
||||||
|
expect(cpu[2]).toBe("p1");
|
||||||
|
// P2 arrives at tick 3 (level 0), preempts P1 (level 1)
|
||||||
|
expect(cpu[3]).toBe("p2");
|
||||||
|
expect(cpu[4]).toBe("p2");
|
||||||
|
// P2 done; P1 resumes at level 1
|
||||||
|
expect(cpu[5]).toBe("p1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Integration — CT-MLFQ-03: I/O return does NOT demote
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ integration — CT-MLFQ-03: no demotion after I/O", () => {
|
||||||
|
it("process returning from I/O stays at the same level (CT-MLFQ-03)", () => {
|
||||||
|
// P1: CPU:1 → IO:2 → CPU:4
|
||||||
|
// P1 uses only 1 tick at level 0 (quantum=2, returns from I/O with qR=1)
|
||||||
|
// After I/O it should still be at level 0 and get a fresh quantum=2 on dispatch
|
||||||
|
const processes = [pWithIo("p1", 0, 1, 2, 4)];
|
||||||
|
const engine = new SimulationEngine(processes, cfg());
|
||||||
|
const ticks = engine.runAll();
|
||||||
|
const cpu = ticks.map((t) => t.cpuProcess);
|
||||||
|
|
||||||
|
// Tick 0: P1 runs (CPU burst 1 tick)
|
||||||
|
expect(cpu[0]).toBe("p1");
|
||||||
|
// Ticks 1-2: P1 in I/O (CPU=null)
|
||||||
|
expect(cpu[1]).toBeNull();
|
||||||
|
expect(cpu[2]).toBeNull();
|
||||||
|
// Ticks 3-6: P1 runs remaining CPU burst (4 ticks)
|
||||||
|
expect(cpu[3]).toBe("p1");
|
||||||
|
expect(cpu[4]).toBe("p1");
|
||||||
|
expect(cpu[5]).toBe("p1");
|
||||||
|
expect(cpu[6]).toBe("p1");
|
||||||
|
|
||||||
|
const rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
|
||||||
|
expect(rt.state).toBe("Terminated");
|
||||||
|
expect(rt.cpuTime).toBe(5);
|
||||||
|
expect(rt.ioTime).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Integration — CT-MLFQ-04: all processes terminate correctly
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQ integration — CT-MLFQ-04: correct termination", () => {
|
||||||
|
it("all processes terminate with correct cpuTime", () => {
|
||||||
|
const processes = [p("p1", 0, 3), p("p2", 0, 5), p("p3", 2, 4)];
|
||||||
|
const engine = new SimulationEngine(processes, cfg());
|
||||||
|
engine.runAll();
|
||||||
|
const rts = engine.runtimeStates;
|
||||||
|
|
||||||
|
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
|
||||||
|
expect(rts.find((r) => r.processId === "p1")!.cpuTime).toBe(3);
|
||||||
|
expect(rts.find((r) => r.processId === "p2")!.cpuTime).toBe(5);
|
||||||
|
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Metadata
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MLFQAlgorithm — metadata", () => {
|
||||||
|
it("exposes correct educational properties", () => {
|
||||||
|
expect(mlfq.name).toContain("MLFQ");
|
||||||
|
expect(mlfq.isPreemptiveCapable).toBe(true);
|
||||||
|
expect(mlfq.usesQuantum).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { ThreadScheduler } from "../thread-scheduler";
|
||||||
|
import type { ThreadRuntime } from "../types";
|
||||||
|
|
||||||
|
function makeThread(
|
||||||
|
threadId: string,
|
||||||
|
arrivalTick = 0,
|
||||||
|
priority = 5,
|
||||||
|
quantumRemaining = 2,
|
||||||
|
): ThreadRuntime {
|
||||||
|
return {
|
||||||
|
threadId,
|
||||||
|
processId: "p1",
|
||||||
|
state: "Ready",
|
||||||
|
remainingBurst: 4,
|
||||||
|
burstIndex: 0,
|
||||||
|
arrivalTick,
|
||||||
|
startTick: null,
|
||||||
|
finishTick: null,
|
||||||
|
waitingTime: 0,
|
||||||
|
cpuTime: 0,
|
||||||
|
ioTime: 0,
|
||||||
|
responseTime: null,
|
||||||
|
quantumRemaining,
|
||||||
|
currentPriority: priority,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ThreadScheduler — política FCFS", () => {
|
||||||
|
const sched = new ThreadScheduler("FCFS", 2);
|
||||||
|
|
||||||
|
it("retorna null para fila vazia", () => {
|
||||||
|
expect(sched.select([])).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("seleciona thread com menor arrivalTick", () => {
|
||||||
|
const t1 = makeThread("t1", 5);
|
||||||
|
const t2 = makeThread("t2", 2);
|
||||||
|
const t3 = makeThread("t3", 8);
|
||||||
|
expect(sched.select([t1, t2, t3])?.threadId).toBe("t2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("desempate lexicográfico por threadId", () => {
|
||||||
|
const ta = makeThread("ta", 0);
|
||||||
|
const tb = makeThread("tb", 0);
|
||||||
|
expect(sched.select([tb, ta])?.threadId).toBe("ta");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isQuantumExpired sempre false em FCFS", () => {
|
||||||
|
const t = makeThread("t1");
|
||||||
|
t.quantumRemaining = -1;
|
||||||
|
expect(sched.isQuantumExpired(t)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldPreemptThread sempre false em FCFS", () => {
|
||||||
|
const curr = makeThread("curr", 0, 5);
|
||||||
|
const cand = makeThread("cand", 0, 1);
|
||||||
|
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ThreadScheduler — política RR", () => {
|
||||||
|
const sched = new ThreadScheduler("RR", 3);
|
||||||
|
|
||||||
|
it("seleciona primeira da fila", () => {
|
||||||
|
const t1 = makeThread("t1");
|
||||||
|
const t2 = makeThread("t2");
|
||||||
|
expect(sched.select([t1, t2])?.threadId).toBe("t1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isQuantumExpired true quando quantumRemaining ≤ 0", () => {
|
||||||
|
const t = makeThread("t1");
|
||||||
|
t.quantumRemaining = 0;
|
||||||
|
expect(sched.isQuantumExpired(t)).toBe(true);
|
||||||
|
t.quantumRemaining = -1;
|
||||||
|
expect(sched.isQuantumExpired(t)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isQuantumExpired false quando quantum ainda disponível", () => {
|
||||||
|
const t = makeThread("t1");
|
||||||
|
t.quantumRemaining = 1;
|
||||||
|
expect(sched.isQuantumExpired(t)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("quantum correto", () => {
|
||||||
|
expect(sched.quantum).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ThreadScheduler — política PRIORITY", () => {
|
||||||
|
const sched = new ThreadScheduler("PRIORITY", 2);
|
||||||
|
|
||||||
|
it("seleciona thread com menor currentPriority (maior prioridade)", () => {
|
||||||
|
const t1 = makeThread("t1", 0, 5);
|
||||||
|
const t2 = makeThread("t2", 0, 1);
|
||||||
|
const t3 = makeThread("t3", 0, 3);
|
||||||
|
expect(sched.select([t1, t2, t3])?.threadId).toBe("t2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("desempate por arrivalTick quando prioridades iguais", () => {
|
||||||
|
const t1 = makeThread("t1", 3, 2);
|
||||||
|
const t2 = makeThread("t2", 1, 2);
|
||||||
|
expect(sched.select([t1, t2])?.threadId).toBe("t2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldPreemptThread true quando candidata tem prioridade maior", () => {
|
||||||
|
const curr = makeThread("curr", 0, 5);
|
||||||
|
const cand = makeThread("cand", 0, 2);
|
||||||
|
expect(sched.shouldPreemptThread(curr, cand)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldPreemptThread false quando prioridade igual", () => {
|
||||||
|
const curr = makeThread("curr", 0, 3);
|
||||||
|
const cand = makeThread("cand", 0, 3);
|
||||||
|
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldPreemptThread false quando candidata tem prioridade menor", () => {
|
||||||
|
const curr = makeThread("curr", 0, 2);
|
||||||
|
const cand = makeThread("cand", 0, 5);
|
||||||
|
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isQuantumExpired sempre false em PRIORITY", () => {
|
||||||
|
const t = makeThread("t1");
|
||||||
|
t.quantumRemaining = -5;
|
||||||
|
expect(sched.isQuantumExpired(t)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -66,4 +66,16 @@ export abstract class BaseAlgorithm {
|
|||||||
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
|
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
|
||||||
return _tiebreak(a, b);
|
return _tiebreak(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna o quantum a ser atribuído ao processo no momento do dispatch.
|
||||||
|
* Algoritmos com quantum por processo (ex.: MLFQ) devem sobrescrever este método.
|
||||||
|
* O padrão usa o quantum global da configuração.
|
||||||
|
*/
|
||||||
|
public getQuantumForProcess(
|
||||||
|
_runtime: ProcessRuntime,
|
||||||
|
config: SchedulerConfig,
|
||||||
|
): number {
|
||||||
|
return config.quantum;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "./p
|
|||||||
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
|
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
|
||||||
import { HRRNAlgorithm } from "./hrrn";
|
import { HRRNAlgorithm } from "./hrrn";
|
||||||
import { MultilevelQueueAlgorithm } from "./multilevel";
|
import { MultilevelQueueAlgorithm } from "./multilevel";
|
||||||
|
import { MLFQAlgorithm } from "./mlfq";
|
||||||
|
|
||||||
export type { BaseAlgorithm };
|
export type { BaseAlgorithm };
|
||||||
|
|
||||||
@@ -28,4 +29,5 @@ export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
|
|||||||
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
||||||
HRRN: new HRRNAlgorithm(),
|
HRRN: new HRRNAlgorithm(),
|
||||||
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
||||||
|
MLFQ: new MLFQAlgorithm(),
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import type { MlfqLevelDef, ProcessRuntime, SchedulerConfig } from "../types";
|
||||||
|
import { BaseAlgorithm } from "./base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MLFQ — Multilevel Feedback Queue (Fila Multinível com Retroalimentação)
|
||||||
|
*
|
||||||
|
* Ao contrário do MLQ, os processos não são atribuídos permanentemente a filas.
|
||||||
|
* Em vez disso, o comportamento de CPU de cada processo determina dinamicamente
|
||||||
|
* em qual nível ele se encontra:
|
||||||
|
*
|
||||||
|
* - Todos os novos processos entram no nível 0 (maior prioridade).
|
||||||
|
* - Quando um processo esgota seu quantum, é rebaixado ao nível seguinte.
|
||||||
|
* - Quando um processo retorna de I/O, permanece no mesmo nível (sem rebaixamento).
|
||||||
|
* - Processos em níveis mais altos preemptam processos em níveis mais baixos.
|
||||||
|
* - Se `mlfqBoostInterval > 0`, processos que esperarem ≥ N ticks são
|
||||||
|
* promovidos ao nível 0 para evitar inanição.
|
||||||
|
*
|
||||||
|
* Cada nível tem seu próprio quantum. O último nível não sofre mais rebaixamentos.
|
||||||
|
* Dentro de um nível, a seleção é FIFO (por arrivalTick, depois processId).
|
||||||
|
*/
|
||||||
|
export class MLFQAlgorithm extends BaseAlgorithm {
|
||||||
|
readonly name = "Multilevel Feedback Queue (MLFQ)";
|
||||||
|
readonly description =
|
||||||
|
"Filas de prioridade dinâmicas: novos processos entram no nível 0 e são " +
|
||||||
|
"rebaixados ao usar todo o quantum. Retorno de I/O não causa rebaixamento. " +
|
||||||
|
"Boost opcional promove processos que esperaram muito tempo de volta ao nível 0.";
|
||||||
|
readonly isPreemptiveCapable = true;
|
||||||
|
readonly usesQuantum = true;
|
||||||
|
|
||||||
|
static readonly DEFAULT_LEVELS: MlfqLevelDef[] = [
|
||||||
|
{ quantum: 2 },
|
||||||
|
{ quantum: 4 },
|
||||||
|
{ quantum: 8 },
|
||||||
|
];
|
||||||
|
|
||||||
|
select(
|
||||||
|
readyQueue: readonly ProcessRuntime[],
|
||||||
|
config: SchedulerConfig,
|
||||||
|
): ProcessRuntime | null {
|
||||||
|
if (readyQueue.length === 0) return null;
|
||||||
|
|
||||||
|
const levels = config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS;
|
||||||
|
const maxLevel = levels.length - 1;
|
||||||
|
const boostInterval = config.mlfqBoostInterval ?? 0;
|
||||||
|
|
||||||
|
// Phase 1 — initialize new processes and apply demotion.
|
||||||
|
// A process with quantumRemaining <= 0 and an already-initialized mlfqLevel
|
||||||
|
// was preempted by quantum expiry → demote it.
|
||||||
|
for (const rt of readyQueue) {
|
||||||
|
if (rt.mlfqLevel === undefined) {
|
||||||
|
// New process: assign level 0.
|
||||||
|
rt.mlfqLevel = 0;
|
||||||
|
} else if (rt.quantumRemaining <= 0) {
|
||||||
|
// Quantum-expired: demote (capped at last level).
|
||||||
|
rt.mlfqLevel = Math.min(rt.mlfqLevel + 1, maxLevel);
|
||||||
|
// Set sentinel so we don't demote again before the next dispatch.
|
||||||
|
rt.quantumRemaining = 1;
|
||||||
|
}
|
||||||
|
// quantumRemaining > 0 → returned from I/O or priority-preempted; no demotion.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2 — priority boost.
|
||||||
|
if (boostInterval > 0) {
|
||||||
|
for (const rt of readyQueue) {
|
||||||
|
if ((rt.mlfqLevel ?? 0) > 0 && rt.waitingTime >= boostInterval) {
|
||||||
|
rt.mlfqLevel = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3 — pick the process at the highest level (lowest index), FIFO within.
|
||||||
|
let best: ProcessRuntime | null = null;
|
||||||
|
for (const rt of readyQueue) {
|
||||||
|
if (best === null) {
|
||||||
|
best = rt;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rtLevel = rt.mlfqLevel ?? 0;
|
||||||
|
const bestLevel = best.mlfqLevel ?? 0;
|
||||||
|
if (
|
||||||
|
rtLevel < bestLevel ||
|
||||||
|
(rtLevel === bestLevel && BaseAlgorithm.tiebreak(rt, best) < 0)
|
||||||
|
) {
|
||||||
|
best = rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldPreempt(
|
||||||
|
current: ProcessRuntime,
|
||||||
|
candidate: ProcessRuntime,
|
||||||
|
_config: SchedulerConfig,
|
||||||
|
): boolean {
|
||||||
|
// A candidate at a higher-priority level (lower index) preempts the running process.
|
||||||
|
return (candidate.mlfqLevel ?? 0) < (current.mlfqLevel ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
isQuantumExpired(
|
||||||
|
runtime: ProcessRuntime,
|
||||||
|
_config: SchedulerConfig,
|
||||||
|
): boolean {
|
||||||
|
return runtime.quantumRemaining <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
override getQuantumForProcess(
|
||||||
|
runtime: ProcessRuntime,
|
||||||
|
config: SchedulerConfig,
|
||||||
|
): number {
|
||||||
|
const levels = config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS;
|
||||||
|
const level = runtime.mlfqLevel ?? 0;
|
||||||
|
return levels[level]?.quantum ?? config.quantum;
|
||||||
|
}
|
||||||
|
}
|
||||||
+677
-129
@@ -4,9 +4,12 @@ import type {
|
|||||||
SchedulerConfig,
|
SchedulerConfig,
|
||||||
SimTick,
|
SimTick,
|
||||||
StateLabel,
|
StateLabel,
|
||||||
|
Thread,
|
||||||
|
ThreadRuntime,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { algorithms } from "./algorithms/index";
|
import { algorithms } from "./algorithms/index";
|
||||||
import type { BaseAlgorithm } from "./algorithms/index";
|
import type { BaseAlgorithm } from "./algorithms/index";
|
||||||
|
import { ThreadScheduler } from "./thread-scheduler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SimulationEngine — Núcleo de execução da simulação de escalonamento.
|
* SimulationEngine — Núcleo de execução da simulação de escalonamento.
|
||||||
@@ -14,42 +17,66 @@ import type { BaseAlgorithm } from "./algorithms/index";
|
|||||||
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
|
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
|
||||||
* de escalonamento configurado e mantendo o estado de todos os processos.
|
* de escalonamento configurado e mantendo o estado de todos os processos.
|
||||||
*
|
*
|
||||||
|
* Suporta dois modos de operação:
|
||||||
|
* - Processos simples: comportamento original, sem alterações.
|
||||||
|
* - Processos com threads: escalonamento em dois níveis:
|
||||||
|
* 1. Nível de processo — algoritmo global do SchedulerConfig.
|
||||||
|
* 2. Nível de thread — ThreadScheduler com política intra-processo.
|
||||||
|
*
|
||||||
|
* Modelos de threads suportados:
|
||||||
|
* MANY_TO_ONE — Uma kernel thread por processo. I/O bloqueia todo o processo.
|
||||||
|
* ONE_TO_ONE — Uma kernel thread por thread de usuário. Threads bloqueiam
|
||||||
|
* independentemente; o processo só bloqueia se todas as threads
|
||||||
|
* estiverem em I/O.
|
||||||
|
* MANY_TO_MANY — M kernel threads para N threads de usuário (M ≤ N).
|
||||||
|
* Slots são liberados durante I/O e reatribuídos dinamicamente.
|
||||||
|
*
|
||||||
* Ciclo de vida de um único tick (método `step()`):
|
* Ciclo de vida de um único tick (método `step()`):
|
||||||
* 1. Chegada — processos cujo `arrivalTime` é igual ao tick atual
|
* 1. Chegada — New → Ready para processos/threads que chegam neste tick.
|
||||||
* transitam New → Ready e entram na fila de prontos.
|
* 2. Preempção — Algoritmo decide se processo em CPU deve ceder lugar.
|
||||||
* 2. Preempção — o algoritmo decide se o processo em CPU deve ceder
|
* 3. Despacho — Seleciona e despacha próximo processo (e thread).
|
||||||
* lugar a um candidato mais prioritário.
|
* 4. Snapshot — Captura estado completo (processos + threads).
|
||||||
* 3. Despacho — seleciona e despacha o próximo processo (com overhead
|
* 5. Passagem do tempo — Decrementa bursts, acumula tempos.
|
||||||
* de troca de contexto, se configurado).
|
* 6. Aging — Envelhecimento de prioridade anti-starvation.
|
||||||
* 4. Snapshot — captura o estado de todos os processos neste tick.
|
* 7. Conclusões — Trata término de bursts de CPU/IO (processo e threads).
|
||||||
* 5. Passagem do tempo — decrementa bursts, acumula tempos de CPU/IO/espera.
|
|
||||||
* 6. Aging — decrementa prioridade dos processos em espera prolongada.
|
|
||||||
* 7. Conclusões — trata término de bursts de CPU e IO; avança para o
|
|
||||||
* próximo burst ou termina o processo.
|
|
||||||
*
|
|
||||||
* Uso típico:
|
|
||||||
* ```ts
|
|
||||||
* const engine = new SimulationEngine(processes, config);
|
|
||||||
* const ticks = engine.runAll(); // executa até todos os processos terminarem
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Invariante: após `runAll()`, `isFinished` é `true` e todos os runtimes
|
|
||||||
* têm `state === "Terminated"`.
|
|
||||||
*/
|
*/
|
||||||
export class SimulationEngine {
|
export class SimulationEngine {
|
||||||
// ─── Campos privados ──────────────────────────────────────────────────────
|
// ─── Campos privados — processos ──────────────────────────────────────────
|
||||||
|
|
||||||
private readonly _processes: Process[];
|
private readonly _processes: Process[];
|
||||||
private readonly _config: SchedulerConfig;
|
private readonly _config: SchedulerConfig;
|
||||||
private readonly _algorithm: BaseAlgorithm;
|
private readonly _algorithm: BaseAlgorithm;
|
||||||
|
|
||||||
private _tick: number; // começa em -1; incrementado no início de step()
|
private _tick: number;
|
||||||
private _runtimes: Map<string, ProcessRuntime>;
|
private _runtimes: Map<string, ProcessRuntime>;
|
||||||
private _cpuProcess: ProcessRuntime | null;
|
private _cpuProcess: ProcessRuntime | null;
|
||||||
private _readyQueue: ProcessRuntime[]; // array ordenado (FIFO para RR)
|
private _readyQueue: ProcessRuntime[];
|
||||||
private _ioQueue: ProcessRuntime[];
|
private _ioQueue: ProcessRuntime[];
|
||||||
private _contextSwitchRemaining: number;
|
private _contextSwitchRemaining: number;
|
||||||
private _prevCpuProcessId: string | null; // para detectar troca de contexto
|
private _prevCpuProcessId: string | null;
|
||||||
|
|
||||||
|
// ─── Campos privados — threads ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** threadRuntimes[processId][threadId] = ThreadRuntime */
|
||||||
|
private _threadRuntimes: Map<string, Map<string, ThreadRuntime>>;
|
||||||
|
|
||||||
|
/** Escalonador intra-processo por processo */
|
||||||
|
private _threadSchedulers: Map<string, ThreadScheduler>;
|
||||||
|
|
||||||
|
/** Thread atualmente ativa na CPU para cada processo com threads */
|
||||||
|
private _activeThreads: Map<string, ThreadRuntime | null>;
|
||||||
|
|
||||||
|
/** Threads em I/O por processo */
|
||||||
|
private _threadIoQueues: Map<string, ThreadRuntime[]>;
|
||||||
|
|
||||||
|
/** Fila de threads prontas intra-processo */
|
||||||
|
private _readyThreadQueues: Map<string, ThreadRuntime[]>;
|
||||||
|
|
||||||
|
/** MANY_TO_MANY: slots de kernel disponíveis por processo */
|
||||||
|
private _kernelSlotsAvailable: Map<string, number>;
|
||||||
|
|
||||||
|
/** MANY_TO_MANY: threads aguardando slot de kernel */
|
||||||
|
private _kernelWaitQueues: Map<string, ThreadRuntime[]>;
|
||||||
|
|
||||||
// ─── Construtor ───────────────────────────────────────────────────────────
|
// ─── Construtor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -64,37 +91,47 @@ export class SimulationEngine {
|
|||||||
this._ioQueue = [];
|
this._ioQueue = [];
|
||||||
this._contextSwitchRemaining = 0;
|
this._contextSwitchRemaining = 0;
|
||||||
this._prevCpuProcessId = null;
|
this._prevCpuProcessId = null;
|
||||||
|
|
||||||
|
// Inicializa runtimes de processo
|
||||||
this._runtimes = new Map(
|
this._runtimes = new Map(
|
||||||
processes.map((p) => [p.id, SimulationEngine._initRuntime(p, config)]),
|
processes.map((p) => [p.id, SimulationEngine._initProcessRuntime(p, config)]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Inicializa estruturas de threads
|
||||||
|
this._threadRuntimes = new Map();
|
||||||
|
this._threadSchedulers = new Map();
|
||||||
|
this._activeThreads = new Map();
|
||||||
|
this._threadIoQueues = new Map();
|
||||||
|
this._readyThreadQueues = new Map();
|
||||||
|
this._kernelSlotsAvailable = new Map();
|
||||||
|
this._kernelWaitQueues = new Map();
|
||||||
|
|
||||||
|
for (const p of processes) {
|
||||||
|
if (this._hasThreads(p.id)) {
|
||||||
|
this._initThreadStructures(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── API pública ──────────────────────────────────────────────────────────
|
// ─── API pública ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Avança a simulação por um tick e retorna o snapshot do estado neste tick.
|
|
||||||
*
|
|
||||||
* Lança um erro se a simulação já tiver terminado (`isFinished === true`).
|
|
||||||
* O número do tick é incrementado no início; o primeiro tick válido é 0.
|
|
||||||
*/
|
|
||||||
step(): SimTick {
|
step(): SimTick {
|
||||||
if (this.isFinished) throw new Error("Simulation is already finished");
|
if (this.isFinished) throw new Error("Simulation is already finished");
|
||||||
|
|
||||||
this._tick++;
|
this._tick++;
|
||||||
|
|
||||||
this._handleArrivals(); // 1. New → Ready para quem chega agora
|
this._handleArrivals();
|
||||||
this._checkPreemption(); // 2. Preempta CPU se candidato melhor chegou
|
this._checkPreemption();
|
||||||
this._dispatchNext(); // 3. Despacha próximo (ou inicia context switch)
|
this._dispatchNext();
|
||||||
const snapshot = this._snapshot(); // 4. Captura estado DURANTE este tick
|
const snapshot = this._snapshot();
|
||||||
this._advanceTime(); // 5. Decrementa bursts, acumula métricas
|
this._advanceTime();
|
||||||
this._applyAging(); // 6. Aging de prioridade (se habilitado)
|
this._applyAging();
|
||||||
this._handleCpuCompletion(); // 7a. Finaliza burst/quantum de CPU
|
this._handleCpuCompletion();
|
||||||
this._handleIoCompletions(); // 7b. Finaliza bursts de I/O
|
this._handleIoCompletions();
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Executa até o fim e retorna todos os snapshots de tick. */
|
|
||||||
runAll(): SimTick[] {
|
runAll(): SimTick[] {
|
||||||
const results: SimTick[] = [];
|
const results: SimTick[] = [];
|
||||||
while (!this.isFinished) {
|
while (!this.isFinished) {
|
||||||
@@ -106,7 +143,6 @@ export class SimulationEngine {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verdadeiro quando todos os processos estão no estado "Terminated". */
|
|
||||||
get isFinished(): boolean {
|
get isFinished(): boolean {
|
||||||
for (const rt of this._runtimes.values()) {
|
for (const rt of this._runtimes.values()) {
|
||||||
if (rt.state !== "Terminated") return false;
|
if (rt.state !== "Terminated") return false;
|
||||||
@@ -114,23 +150,34 @@ export class SimulationEngine {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Número do tick atual (−1 antes do primeiro `step()`). */
|
|
||||||
get currentTick(): number {
|
get currentTick(): number {
|
||||||
return this._tick;
|
return this._tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Array com o estado de runtime de todos os processos. */
|
|
||||||
get runtimeStates(): ProcessRuntime[] {
|
get runtimeStates(): ProcessRuntime[] {
|
||||||
return Array.from(this._runtimes.values());
|
return Array.from(this._runtimes.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retorna todos os runtimes de threads de todos os processos. */
|
||||||
|
get threadRuntimeStates(): ThreadRuntime[] {
|
||||||
|
const result: ThreadRuntime[] = [];
|
||||||
|
for (const threadMap of this._threadRuntimes.values()) {
|
||||||
|
for (const trt of threadMap.values()) {
|
||||||
|
result.push(trt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Informa se um processo possui threads definidas. */
|
||||||
|
processHasThreads(processId: string): boolean {
|
||||||
|
return this._hasThreads(processId);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Fases do ciclo de simulação ──────────────────────────────────────────
|
// ─── Fases do ciclo de simulação ──────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 1 — Chegada de processos.
|
* Fase 1 — Chegada de processos (e inicialização de threads).
|
||||||
*
|
|
||||||
* Processos cujo `arrivalTime` é igual ao tick atual transitam do estado
|
|
||||||
* "New" para "Ready" e são inseridos na fila de prontos.
|
|
||||||
*/
|
*/
|
||||||
private _handleArrivals(): void {
|
private _handleArrivals(): void {
|
||||||
for (const p of this._processes) {
|
for (const p of this._processes) {
|
||||||
@@ -138,17 +185,40 @@ export class SimulationEngine {
|
|||||||
if (p.arrivalTime === this._tick && rt.state === "New") {
|
if (p.arrivalTime === this._tick && rt.state === "New") {
|
||||||
this._transition(rt, "Ready");
|
this._transition(rt, "Ready");
|
||||||
this._readyQueue.push(rt);
|
this._readyQueue.push(rt);
|
||||||
|
|
||||||
|
if (this._hasThreads(p.id)) {
|
||||||
|
// Transiciona todas as threads New → Ready na fila intra-processo
|
||||||
|
const threadMap = this._threadRuntimes.get(p.id)!;
|
||||||
|
const readyQueue = this._readyThreadQueues.get(p.id)!;
|
||||||
|
const model = p.threadModel ?? "ONE_TO_ONE";
|
||||||
|
|
||||||
|
for (const trt of threadMap.values()) {
|
||||||
|
if (trt.state === "New") {
|
||||||
|
if (model === "MANY_TO_MANY") {
|
||||||
|
const slotsAvail = this._kernelSlotsAvailable.get(p.id) ?? 0;
|
||||||
|
if (slotsAvail > 0) {
|
||||||
|
this._kernelSlotsAvailable.set(p.id, slotsAvail - 1);
|
||||||
|
this._transitionThread(trt, "Ready");
|
||||||
|
readyQueue.push(trt);
|
||||||
|
} else {
|
||||||
|
// Sem slot disponível → espera kernel thread
|
||||||
|
trt.kernelWaiting = true;
|
||||||
|
this._transitionThread(trt, "Ready");
|
||||||
|
this._kernelWaitQueues.get(p.id)!.push(trt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._transitionThread(trt, "Ready");
|
||||||
|
readyQueue.push(trt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 2 — Verificação de preempção.
|
* Fase 2 — Verificação de preempção no nível de processo.
|
||||||
*
|
|
||||||
* Consulta o algoritmo: se houver um candidato na fila de prontos que deva
|
|
||||||
* preemptar o processo em CPU, o processo atual retorna à fila de prontos
|
|
||||||
* e `_cpuProcess` é zerado. Chamado após a chegada para que processos
|
|
||||||
* recém-chegados possam imediatamente preemptar.
|
|
||||||
*/
|
*/
|
||||||
private _checkPreemption(): void {
|
private _checkPreemption(): void {
|
||||||
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
|
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
|
||||||
@@ -158,6 +228,17 @@ export class SimulationEngine {
|
|||||||
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
||||||
) {
|
) {
|
||||||
const cpu = this._cpuProcess;
|
const cpu = this._cpuProcess;
|
||||||
|
|
||||||
|
// Retorna thread ativa à fila intra-processo (se processo com threads)
|
||||||
|
if (this._hasThreads(cpu.processId)) {
|
||||||
|
const activeThread = this._activeThreads.get(cpu.processId) ?? null;
|
||||||
|
if (activeThread !== null) {
|
||||||
|
this._transitionThread(activeThread, "Ready");
|
||||||
|
this._readyThreadQueues.get(cpu.processId)!.unshift(activeThread);
|
||||||
|
this._activeThreads.set(cpu.processId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._transition(cpu, "Ready");
|
this._transition(cpu, "Ready");
|
||||||
this._readyQueue.push(cpu);
|
this._readyQueue.push(cpu);
|
||||||
this._cpuProcess = null;
|
this._cpuProcess = null;
|
||||||
@@ -165,16 +246,7 @@ export class SimulationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 3 — Despacho / troca de contexto.
|
* Fase 3 — Despacho de processo (e thread).
|
||||||
*
|
|
||||||
* Se um context switch estiver em andamento, nenhum processo é despachado
|
|
||||||
* neste tick. Caso contrário, e se a CPU estiver ociosa com processos
|
|
||||||
* prontos, o algoritmo seleciona o próximo processo:
|
|
||||||
*
|
|
||||||
* - Se houver overhead de troca de contexto configurado e o processo
|
|
||||||
* selecionado for diferente do último, o processo volta ao início da
|
|
||||||
* fila e o contador de context-switch é iniciado.
|
|
||||||
* - Caso contrário, o processo é despachado imediatamente para a CPU.
|
|
||||||
*/
|
*/
|
||||||
private _dispatchNext(): void {
|
private _dispatchNext(): void {
|
||||||
if (this._contextSwitchRemaining > 0) return;
|
if (this._contextSwitchRemaining > 0) return;
|
||||||
@@ -196,21 +268,71 @@ export class SimulationEngine {
|
|||||||
this._prevCpuProcessId = next.processId;
|
this._prevCpuProcessId = next.processId;
|
||||||
} else {
|
} else {
|
||||||
if (next.startTick === null) next.startTick = this._tick;
|
if (next.startTick === null) next.startTick = this._tick;
|
||||||
next.quantumRemaining = this._config.quantum;
|
next.quantumRemaining = this._algorithm.getQuantumForProcess(next, this._config);
|
||||||
this._transition(next, "Running");
|
this._transition(next, "Running");
|
||||||
this._prevCpuProcessId = next.processId;
|
this._prevCpuProcessId = next.processId;
|
||||||
this._cpuProcess = next;
|
this._cpuProcess = next;
|
||||||
|
|
||||||
|
// Despacha thread intra-processo (se aplicável)
|
||||||
|
if (this._hasThreads(next.processId)) {
|
||||||
|
this._dispatchThreadForProcess(next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fase 4 — Snapshot do estado.
|
||||||
|
*/
|
||||||
|
private _snapshot(): SimTick {
|
||||||
|
const states: Record<string, StateLabel> = {};
|
||||||
|
for (const [id, rt] of this._runtimes) {
|
||||||
|
states[id] = rt.state;
|
||||||
|
}
|
||||||
|
const contextSwitching = this._contextSwitchRemaining > 0;
|
||||||
|
|
||||||
|
const snap: SimTick = {
|
||||||
|
tick: this._tick,
|
||||||
|
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
|
||||||
|
ioProcesses: this._ioQueue.map((rt) => rt.processId),
|
||||||
|
readyQueue: this._readyQueue.map((rt) => rt.processId),
|
||||||
|
states,
|
||||||
|
contextSwitching,
|
||||||
|
ctxSwitchForProcess: contextSwitching
|
||||||
|
? (this._readyQueue[0]?.processId ?? null)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inclui estado de threads se houver processos com threads
|
||||||
|
if (this._threadRuntimes.size > 0) {
|
||||||
|
const threadStates: Record<string, Record<string, StateLabel>> = {};
|
||||||
|
const cpuThreads: Record<string, string | null> = {};
|
||||||
|
const kernelWaitingThreads: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const [processId, threadMap] of this._threadRuntimes) {
|
||||||
|
threadStates[processId] = {};
|
||||||
|
for (const [threadId, trt] of threadMap) {
|
||||||
|
threadStates[processId][threadId] = trt.state;
|
||||||
|
}
|
||||||
|
const active = this._activeThreads.get(processId) ?? null;
|
||||||
|
cpuThreads[processId] = active?.threadId ?? null;
|
||||||
|
const kwQueue = this._kernelWaitQueues.get(processId) ?? [];
|
||||||
|
if (kwQueue.length > 0) {
|
||||||
|
kernelWaitingThreads[processId] = kwQueue.map((t) => t.threadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snap.threadStates = threadStates;
|
||||||
|
snap.cpuThreads = cpuThreads;
|
||||||
|
if (Object.keys(kernelWaitingThreads).length > 0) {
|
||||||
|
snap.kernelWaitingThreads = kernelWaitingThreads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 5 — Passagem do tempo.
|
* Fase 5 — Passagem do tempo.
|
||||||
*
|
|
||||||
* Simula a execução de 1 tick:
|
|
||||||
* - Decrementa o contador de context-switch restante (se ativo).
|
|
||||||
* - Decrementa o burst restante do processo em CPU e acumula cpuTime/quantum.
|
|
||||||
* - Decrementa o burst restante de cada processo em I/O e acumula ioTime.
|
|
||||||
* - Incrementa o waitingTime de todos os processos na fila de prontos.
|
|
||||||
*/
|
*/
|
||||||
private _advanceTime(): void {
|
private _advanceTime(): void {
|
||||||
if (this._contextSwitchRemaining > 0) {
|
if (this._contextSwitchRemaining > 0) {
|
||||||
@@ -219,29 +341,67 @@ export class SimulationEngine {
|
|||||||
|
|
||||||
if (this._cpuProcess !== null) {
|
if (this._cpuProcess !== null) {
|
||||||
const cpu = this._cpuProcess;
|
const cpu = this._cpuProcess;
|
||||||
|
|
||||||
|
if (this._hasThreads(cpu.processId)) {
|
||||||
|
// CPU time acumulado no nível de processo
|
||||||
|
cpu.cpuTime++;
|
||||||
|
cpu.quantumRemaining--;
|
||||||
|
|
||||||
|
// Decrementa burst e tempo da thread ativa
|
||||||
|
const activeThread = this._activeThreads.get(cpu.processId) ?? null;
|
||||||
|
if (activeThread !== null) {
|
||||||
|
activeThread.remainingBurst--;
|
||||||
|
activeThread.cpuTime++;
|
||||||
|
activeThread.quantumRemaining--;
|
||||||
|
// Sincroniza processo com thread ativa
|
||||||
|
cpu.remainingBurst = activeThread.remainingBurst;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
cpu.remainingBurst--;
|
cpu.remainingBurst--;
|
||||||
cpu.cpuTime++;
|
cpu.cpuTime++;
|
||||||
cpu.quantumRemaining--;
|
cpu.quantumRemaining--;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// I/O de processos sem threads
|
||||||
for (const rt of this._ioQueue) {
|
for (const rt of this._ioQueue) {
|
||||||
|
if (!this._hasThreads(rt.processId)) {
|
||||||
rt.remainingBurst--;
|
rt.remainingBurst--;
|
||||||
rt.ioTime++;
|
rt.ioTime++;
|
||||||
}
|
}
|
||||||
|
// Processos com threads (MANY_TO_ONE): tempo de I/O acumulado abaixo
|
||||||
|
}
|
||||||
|
|
||||||
|
// I/O de threads (todos os modelos)
|
||||||
|
for (const [, threadIoQueue] of this._threadIoQueues) {
|
||||||
|
for (const trt of threadIoQueue) {
|
||||||
|
trt.remainingBurst--;
|
||||||
|
trt.ioTime++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tempo de espera — fila de prontos de processos
|
||||||
for (const rt of this._readyQueue) {
|
for (const rt of this._readyQueue) {
|
||||||
rt.waitingTime++;
|
rt.waitingTime++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tempo de espera — fila de prontas de threads intra-processo
|
||||||
|
for (const [, threadReadyQueue] of this._readyThreadQueues) {
|
||||||
|
for (const trt of threadReadyQueue) {
|
||||||
|
trt.waitingTime++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tempo de espera — threads em kernel wait (MANY_TO_MANY)
|
||||||
|
for (const [, kwQueue] of this._kernelWaitQueues) {
|
||||||
|
for (const trt of kwQueue) {
|
||||||
|
trt.waitingTime++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 6 — Aging (envelhecimento de prioridade).
|
* Fase 6 — Aging de prioridade.
|
||||||
*
|
|
||||||
* Quando o aging está habilitado, a cada `agingInterval` ticks o valor de
|
|
||||||
* `currentPriority` de todos os processos na fila de prontos é decrementado
|
|
||||||
* em 1 (até o mínimo de 1). Isso previne inanição em algoritmos de prioridade.
|
|
||||||
*
|
|
||||||
* Não é executado no tick 0 para evitar decremento prematuro.
|
|
||||||
*/
|
*/
|
||||||
private _applyAging(): void {
|
private _applyAging(): void {
|
||||||
if (
|
if (
|
||||||
@@ -258,24 +418,20 @@ export class SimulationEngine {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 7a — Conclusão de burst de CPU.
|
* Fase 7a — Conclusão de burst de CPU.
|
||||||
*
|
|
||||||
* Após a passagem do tempo, verifica se o processo em CPU terminou seu burst
|
|
||||||
* ou esgotou o quantum:
|
|
||||||
*
|
|
||||||
* - Burst zerado: avança para o próximo burst do processo.
|
|
||||||
* · Se o próximo for I/O → transita para "Waiting" e entra na ioQueue.
|
|
||||||
* · Se o próximo for CPU → retorna à fila de prontos ("Ready").
|
|
||||||
* · Se não houver próximo → termina o processo ("Terminated").
|
|
||||||
*
|
|
||||||
* - Quantum expirado (sem término de burst): retorna à fila de prontos.
|
|
||||||
*/
|
*/
|
||||||
private _handleCpuCompletion(): void {
|
private _handleCpuCompletion(): void {
|
||||||
if (this._cpuProcess === null) return;
|
if (this._cpuProcess === null) return;
|
||||||
|
|
||||||
const cpu = this._cpuProcess;
|
const cpu = this._cpuProcess;
|
||||||
|
|
||||||
|
if (this._hasThreads(cpu.processId)) {
|
||||||
|
this._handleThreadedCpuCompletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Processo sem threads (comportamento original) ──────────────────────
|
||||||
if (cpu.remainingBurst === 0) {
|
if (cpu.remainingBurst === 0) {
|
||||||
this._advanceBurst(cpu);
|
this._advanceBurst(cpu);
|
||||||
|
cpu.quantumRemaining = 1;
|
||||||
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
||||||
const nextBurst = process.bursts[cpu.burstIndex];
|
const nextBurst = process.bursts[cpu.burstIndex];
|
||||||
if (nextBurst !== undefined && nextBurst.type === "io") {
|
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||||
@@ -298,16 +454,16 @@ export class SimulationEngine {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 7b — Conclusão de bursts de I/O.
|
* Fase 7b — Conclusão de bursts de I/O.
|
||||||
*
|
|
||||||
* Percorre a ioQueue e, para cada processo cujo burst de I/O zerou:
|
|
||||||
* - Se houver próximo burst → transita para "Ready" e insere na readyQueue.
|
|
||||||
* - Caso contrário → termina o processo ("Terminated").
|
|
||||||
*
|
|
||||||
* Processos ainda com I/O pendente permanecem na ioQueue.
|
|
||||||
*/
|
*/
|
||||||
private _handleIoCompletions(): void {
|
private _handleIoCompletions(): void {
|
||||||
|
// ── Processos sem threads ──────────────────────────────────────────────
|
||||||
const nextIoQueue: ProcessRuntime[] = [];
|
const nextIoQueue: ProcessRuntime[] = [];
|
||||||
for (const rt of this._ioQueue) {
|
for (const rt of this._ioQueue) {
|
||||||
|
if (this._hasThreads(rt.processId)) {
|
||||||
|
// Processos com threads gerenciados em _handleThreadIoCompletions
|
||||||
|
nextIoQueue.push(rt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (rt.remainingBurst === 0) {
|
if (rt.remainingBurst === 0) {
|
||||||
this._advanceBurst(rt);
|
this._advanceBurst(rt);
|
||||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||||
@@ -323,22 +479,359 @@ export class SimulationEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._ioQueue = nextIoQueue;
|
this._ioQueue = nextIoQueue;
|
||||||
|
|
||||||
|
// ── Threads em I/O ────────────────────────────────────────────────────
|
||||||
|
this._handleThreadIoCompletions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Lógica de threads ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Despacha a próxima thread intra-processo quando o processo recebe CPU.
|
||||||
|
*/
|
||||||
|
private _dispatchThreadForProcess(processRt: ProcessRuntime): void {
|
||||||
|
const processId = processRt.processId;
|
||||||
|
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
|
||||||
|
const scheduler = this._threadSchedulers.get(processId)!;
|
||||||
|
|
||||||
|
const nextThread = scheduler.select(readyThreadQueue);
|
||||||
|
if (nextThread === null) {
|
||||||
|
// Nenhuma thread pronta — mantém processo em Running mas sem thread ativa
|
||||||
|
// (Pode ocorrer se todas as threads estão em I/O; ONE_TO_ONE pode despachar
|
||||||
|
// o processo mesmo assim para que ele "exista" na CPU enquanto threads esperam)
|
||||||
|
this._activeThreads.set(processId, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = readyThreadQueue.indexOf(nextThread);
|
||||||
|
if (idx !== -1) readyThreadQueue.splice(idx, 1);
|
||||||
|
|
||||||
|
if (nextThread.startTick === null) {
|
||||||
|
nextThread.startTick = this._tick;
|
||||||
|
nextThread.responseTime = this._tick - nextThread.arrivalTick;
|
||||||
|
}
|
||||||
|
nextThread.quantumRemaining = scheduler.quantum;
|
||||||
|
nextThread.kernelWaiting = false;
|
||||||
|
this._transitionThread(nextThread, "Running");
|
||||||
|
this._activeThreads.set(processId, nextThread);
|
||||||
|
|
||||||
|
// Sincroniza process runtime com thread ativa
|
||||||
|
processRt.remainingBurst = nextThread.remainingBurst;
|
||||||
|
if (processRt.startTick === null) {
|
||||||
|
processRt.startTick = this._tick;
|
||||||
|
processRt.responseTime = this._tick - processRt.arrivalTick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gerencia conclusão de burst de CPU para processos com threads.
|
||||||
|
*/
|
||||||
|
private _handleThreadedCpuCompletion(): void {
|
||||||
|
const cpu = this._cpuProcess!;
|
||||||
|
const processId = cpu.processId;
|
||||||
|
const process = this._processes.find((p) => p.id === processId)!;
|
||||||
|
const policy = process.threadSchedulingPolicy ?? "FCFS";
|
||||||
|
const scheduler = this._threadSchedulers.get(processId)!;
|
||||||
|
const activeThread = this._activeThreads.get(processId) ?? null;
|
||||||
|
|
||||||
|
if (activeThread === null) {
|
||||||
|
// Sem thread ativa — verifica se processo deve continuar ou aguardar
|
||||||
|
if (this._readyThreadQueues.get(processId)!.length > 0) {
|
||||||
|
this._dispatchThreadForProcess(cpu);
|
||||||
|
} else {
|
||||||
|
// Todas as threads em I/O: processo vai para Waiting
|
||||||
|
this._transition(cpu, "Waiting");
|
||||||
|
if (!this._ioQueue.includes(cpu)) this._ioQueue.push(cpu);
|
||||||
|
this._cpuProcess = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeThread.remainingBurst === 0) {
|
||||||
|
// ── CPU burst da thread concluído ────────────────────────────────────
|
||||||
|
this._activeThreads.set(processId, null);
|
||||||
|
this._handleThreadBurstComplete(cpu, activeThread, process);
|
||||||
|
|
||||||
|
// Se o quantum do processo também expirou neste mesmo tick, cede a CPU
|
||||||
|
// agora (após tratar o burst) em vez de dar um tick extra ao processo.
|
||||||
|
if (this._cpuProcess !== null && this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||||
|
const activeAfter = this._activeThreads.get(processId) ?? null;
|
||||||
|
if (activeAfter !== null) {
|
||||||
|
this._transitionThread(activeAfter, "Ready");
|
||||||
|
if (policy === "RR") {
|
||||||
|
this._readyThreadQueues.get(processId)!.push(activeAfter);
|
||||||
|
} else {
|
||||||
|
this._readyThreadQueues.get(processId)!.unshift(activeAfter);
|
||||||
|
}
|
||||||
|
this._activeThreads.set(processId, null);
|
||||||
|
}
|
||||||
|
this._transition(cpu, "Ready");
|
||||||
|
this._readyQueue.push(cpu);
|
||||||
|
this._cpuProcess = null;
|
||||||
|
}
|
||||||
|
} else if (scheduler.isQuantumExpired(activeThread)) {
|
||||||
|
// ── Quantum intra-processo expirou (RR): rotaciona thread ────────────
|
||||||
|
this._transitionThread(activeThread, "Ready");
|
||||||
|
this._readyThreadQueues.get(processId)!.push(activeThread); // reinserido no fim (FIFO)
|
||||||
|
this._activeThreads.set(processId, null);
|
||||||
|
|
||||||
|
// Seleciona próxima thread — processo permanece na CPU
|
||||||
|
this._dispatchThreadForProcess(cpu);
|
||||||
|
|
||||||
|
// Se não há próxima thread pronta, processo aguarda
|
||||||
|
if (this._activeThreads.get(processId) === null) {
|
||||||
|
this._transition(cpu, "Waiting");
|
||||||
|
if (!this._ioQueue.includes(cpu)) this._ioQueue.push(cpu);
|
||||||
|
this._cpuProcess = null;
|
||||||
|
}
|
||||||
|
} else if (this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||||
|
// ── Quantum de processo expirou: devolve processo à fila global ───────
|
||||||
|
// Thread volta à fila intra-processo
|
||||||
|
this._transitionThread(activeThread, "Ready");
|
||||||
|
if (policy === "RR") {
|
||||||
|
this._readyThreadQueues.get(processId)!.push(activeThread);
|
||||||
|
} else {
|
||||||
|
this._readyThreadQueues.get(processId)!.unshift(activeThread);
|
||||||
|
}
|
||||||
|
this._activeThreads.set(processId, null);
|
||||||
|
this._transition(cpu, "Ready");
|
||||||
|
this._readyQueue.push(cpu);
|
||||||
|
this._cpuProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trata conclusão do burst de CPU de uma thread específica.
|
||||||
|
* Decide o próximo estado da thread e do processo conforme o modelo.
|
||||||
|
*/
|
||||||
|
private _handleThreadBurstComplete(
|
||||||
|
processRt: ProcessRuntime,
|
||||||
|
thread: ThreadRuntime,
|
||||||
|
process: Process,
|
||||||
|
): void {
|
||||||
|
const processId = process.id;
|
||||||
|
const model = process.threadModel ?? "ONE_TO_ONE";
|
||||||
|
const threadDef = process.threads!.find((t) => t.tid === thread.threadId)!;
|
||||||
|
|
||||||
|
// Avança ao próximo burst da thread
|
||||||
|
thread.burstIndex++;
|
||||||
|
const nextBurst = threadDef.bursts[thread.burstIndex];
|
||||||
|
thread.quantumRemaining = 1; // sentinela anti-falsa-demoção
|
||||||
|
|
||||||
|
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||||
|
// ── Thread vai para I/O ───────────────────────────────────────────────
|
||||||
|
thread.remainingBurst = nextBurst.duration;
|
||||||
|
this._transitionThread(thread, "Waiting");
|
||||||
|
this._threadIoQueues.get(processId)!.push(thread);
|
||||||
|
|
||||||
|
// Para MANY_TO_MANY: libera slot de kernel
|
||||||
|
if (model === "MANY_TO_MANY") {
|
||||||
|
this._releaseKernelSlot(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model === "MANY_TO_ONE") {
|
||||||
|
// Kernel thread bloqueado → bloqueia todo o processo
|
||||||
|
this._transition(processRt, "Waiting");
|
||||||
|
this._ioQueue.push(processRt);
|
||||||
|
this._cpuProcess = null;
|
||||||
|
} else {
|
||||||
|
// ONE_TO_ONE / MANY_TO_MANY: verifica se há outra thread pronta
|
||||||
|
this._trySelectNextThreadOrBlock(processRt, process);
|
||||||
|
}
|
||||||
|
} else if (nextBurst !== undefined && nextBurst.type === "cpu") {
|
||||||
|
// ── Thread tem próximo burst de CPU ───────────────────────────────────
|
||||||
|
thread.remainingBurst = nextBurst.duration;
|
||||||
|
this._transitionThread(thread, "Ready");
|
||||||
|
this._readyThreadQueues.get(processId)!.push(thread);
|
||||||
|
// Processo permanece na CPU; seleciona próxima thread
|
||||||
|
this._dispatchThreadForProcess(processRt);
|
||||||
|
} else {
|
||||||
|
// ── Thread terminada ──────────────────────────────────────────────────
|
||||||
|
this._transitionThread(thread, "Terminated");
|
||||||
|
thread.finishTick = this._tick + 1;
|
||||||
|
|
||||||
|
// Para MANY_TO_MANY: libera slot de kernel
|
||||||
|
if (model === "MANY_TO_MANY") {
|
||||||
|
this._releaseKernelSlot(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se processo deve terminar ou continuar com outra thread
|
||||||
|
if (this._allThreadsTerminated(processId)) {
|
||||||
|
this._transition(processRt, "Terminated");
|
||||||
|
processRt.finishTick = this._tick + 1;
|
||||||
|
this._cpuProcess = null;
|
||||||
|
} else if (model === "MANY_TO_ONE") {
|
||||||
|
// Seleciona próxima thread pronta (se houver)
|
||||||
|
this._trySelectNextThreadOrBlock(processRt, process);
|
||||||
|
} else {
|
||||||
|
this._trySelectNextThreadOrBlock(processRt, process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenta selecionar a próxima thread pronta para o processo.
|
||||||
|
* Se não houver threads prontas e há threads em I/O: processo aguarda.
|
||||||
|
* Se não houver threads prontas e todas terminadas: processo termina.
|
||||||
|
*/
|
||||||
|
private _trySelectNextThreadOrBlock(
|
||||||
|
processRt: ProcessRuntime,
|
||||||
|
process: Process,
|
||||||
|
): void {
|
||||||
|
const processId = process.id;
|
||||||
|
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
|
||||||
|
|
||||||
|
if (readyThreadQueue.length > 0) {
|
||||||
|
// Há threads prontas: seleciona a próxima
|
||||||
|
this._dispatchThreadForProcess(processRt);
|
||||||
|
} else if (this._allThreadsTerminated(processId)) {
|
||||||
|
this._transition(processRt, "Terminated");
|
||||||
|
processRt.finishTick = this._tick + 1;
|
||||||
|
this._cpuProcess = null;
|
||||||
|
} else {
|
||||||
|
// Todas as threads em I/O: processo aguarda
|
||||||
|
this._transition(processRt, "Waiting");
|
||||||
|
if (!this._ioQueue.includes(processRt)) {
|
||||||
|
this._ioQueue.push(processRt);
|
||||||
|
}
|
||||||
|
this._cpuProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gerencia conclusão de bursts de I/O para threads.
|
||||||
|
*/
|
||||||
|
private _handleThreadIoCompletions(): void {
|
||||||
|
for (const [processId, threadIoQueue] of this._threadIoQueues) {
|
||||||
|
const processRt = this._runtimes.get(processId)!;
|
||||||
|
const process = this._processes.find((p) => p.id === processId)!;
|
||||||
|
const model = process.threadModel ?? "ONE_TO_ONE";
|
||||||
|
|
||||||
|
const remaining: ThreadRuntime[] = [];
|
||||||
|
|
||||||
|
for (const trt of threadIoQueue) {
|
||||||
|
if (trt.remainingBurst === 0) {
|
||||||
|
// ── I/O da thread concluído ────────────────────────────────────────
|
||||||
|
const threadDef = process.threads!.find((t) => t.tid === trt.threadId)!;
|
||||||
|
trt.burstIndex++;
|
||||||
|
const nextBurst = threadDef.bursts[trt.burstIndex];
|
||||||
|
trt.quantumRemaining = 1; // sentinela
|
||||||
|
|
||||||
|
if (nextBurst !== undefined && nextBurst.type === "cpu") {
|
||||||
|
trt.remainingBurst = nextBurst.duration;
|
||||||
|
|
||||||
|
if (model === "MANY_TO_MANY") {
|
||||||
|
// Tenta adquirir slot de kernel
|
||||||
|
const slotsAvail = this._kernelSlotsAvailable.get(processId) ?? 0;
|
||||||
|
if (slotsAvail > 0) {
|
||||||
|
this._kernelSlotsAvailable.set(processId, slotsAvail - 1);
|
||||||
|
trt.kernelWaiting = false;
|
||||||
|
this._transitionThread(trt, "Ready");
|
||||||
|
this._readyThreadQueues.get(processId)!.push(trt);
|
||||||
|
} else {
|
||||||
|
// Sem slot: vai para kernel wait queue
|
||||||
|
trt.kernelWaiting = true;
|
||||||
|
this._kernelWaitQueues.get(processId)!.push(trt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._transitionThread(trt, "Ready");
|
||||||
|
this._readyThreadQueues.get(processId)!.push(trt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Thread termina após I/O (ou sem próximo burst)
|
||||||
|
this._transitionThread(trt, "Terminated");
|
||||||
|
trt.finishTick = this._tick + 1;
|
||||||
|
if (model === "MANY_TO_MANY") {
|
||||||
|
// Não havia slot em uso durante I/O; nada a liberar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide estado do processo com base no modelo
|
||||||
|
this._updateProcessStateAfterThreadIo(processRt, process, model);
|
||||||
|
} else {
|
||||||
|
remaining.push(trt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._threadIoQueues.set(processId, remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atualiza o estado do processo após conclusão de I/O de uma thread.
|
||||||
|
*/
|
||||||
|
private _updateProcessStateAfterThreadIo(
|
||||||
|
processRt: ProcessRuntime,
|
||||||
|
process: Process,
|
||||||
|
model: string,
|
||||||
|
): void {
|
||||||
|
const processId = process.id;
|
||||||
|
|
||||||
|
if (model === "MANY_TO_ONE") {
|
||||||
|
// O processo estava bloqueado (Waiting) devido ao I/O da thread
|
||||||
|
// Agora a thread está pronta → processo volta para Ready
|
||||||
|
if (processRt.state === "Waiting") {
|
||||||
|
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
|
||||||
|
this._transition(processRt, "Ready");
|
||||||
|
this._readyQueue.push(processRt);
|
||||||
|
this._activeThreads.set(processId, null); // será selecionada no dispatch
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ONE_TO_ONE / MANY_TO_MANY
|
||||||
|
if (processRt.state === "Waiting") {
|
||||||
|
// Processo estava aguardando porque todas as threads estavam em I/O
|
||||||
|
// Verifica se alguma thread ficou pronta
|
||||||
|
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
|
||||||
|
if (readyThreadQueue.length > 0) {
|
||||||
|
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
|
||||||
|
this._transition(processRt, "Ready");
|
||||||
|
this._readyQueue.push(processRt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Se processo está Running: a thread retornou; será selecionada na próxima rodada
|
||||||
|
// Se processo está Ready: a thread apenas entra na fila intra-processo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se todas as threads terminaram
|
||||||
|
if (this._allThreadsTerminated(processId) && processRt.state !== "Terminated") {
|
||||||
|
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
|
||||||
|
this._readyQueue = this._readyQueue.filter((r) => r.processId !== processId);
|
||||||
|
this._transition(processRt, "Terminated");
|
||||||
|
processRt.finishTick = this._tick + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── MANY_TO_MANY — gerenciamento de slots de kernel ─────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Libera um slot de kernel e tenta promover a próxima thread da kernel wait queue.
|
||||||
|
*/
|
||||||
|
private _releaseKernelSlot(processId: string): void {
|
||||||
|
const kwQueue = this._kernelWaitQueues.get(processId)!;
|
||||||
|
if (kwQueue.length > 0) {
|
||||||
|
// Repassa o slot diretamente para a próxima thread aguardando
|
||||||
|
const next = kwQueue.shift()!;
|
||||||
|
next.kernelWaiting = false;
|
||||||
|
this._transitionThread(next, "Ready");
|
||||||
|
this._readyThreadQueues.get(processId)!.push(next);
|
||||||
|
} else {
|
||||||
|
this._kernelSlotsAvailable.set(
|
||||||
|
processId,
|
||||||
|
(this._kernelSlotsAvailable.get(processId) ?? 0) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers internos ─────────────────────────────────────────────────────
|
// ─── Helpers internos ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Aplica uma transição de estado a um runtime, atualizando `state`.
|
|
||||||
* Centraliza todas as transições para facilitar rastreamento e depuração.
|
|
||||||
*/
|
|
||||||
private _transition(rt: ProcessRuntime, state: StateLabel): void {
|
private _transition(rt: ProcessRuntime, state: StateLabel): void {
|
||||||
rt.state = state;
|
rt.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _transitionThread(trt: ThreadRuntime, state: StateLabel): void {
|
||||||
* Avança o índice de burst de um runtime e atualiza `remainingBurst`
|
trt.state = state;
|
||||||
* com a duração do próximo burst. Se não houver próximo burst, `remainingBurst` é 0.
|
}
|
||||||
*/
|
|
||||||
private _advanceBurst(rt: ProcessRuntime): void {
|
private _advanceBurst(rt: ProcessRuntime): void {
|
||||||
rt.burstIndex++;
|
rt.burstIndex++;
|
||||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||||
@@ -346,45 +839,36 @@ export class SimulationEngine {
|
|||||||
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
|
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _hasThreads(processId: string): boolean {
|
||||||
* Captura o estado instantâneo de todos os processos no tick atual.
|
const p = this._processes.find((proc) => proc.id === processId);
|
||||||
* Se um context switch estiver em andamento, `cpuProcess` é null e
|
return (p?.threads?.length ?? 0) > 0;
|
||||||
* `ctxSwitchForProcess` identifica o processo prestes a executar.
|
|
||||||
*/
|
|
||||||
private _snapshot(): SimTick {
|
|
||||||
const states: Record<string, StateLabel> = {};
|
|
||||||
for (const [id, rt] of this._runtimes) {
|
|
||||||
states[id] = rt.state;
|
|
||||||
}
|
}
|
||||||
const contextSwitching = this._contextSwitchRemaining > 0;
|
|
||||||
return {
|
private _allThreadsTerminated(processId: string): boolean {
|
||||||
tick: this._tick,
|
const threadMap = this._threadRuntimes.get(processId);
|
||||||
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
|
if (!threadMap) return true;
|
||||||
ioProcesses: this._ioQueue.map((rt) => rt.processId),
|
for (const trt of threadMap.values()) {
|
||||||
readyQueue: this._readyQueue.map((rt) => rt.processId),
|
if (trt.state !== "Terminated") return false;
|
||||||
states,
|
}
|
||||||
contextSwitching,
|
return true;
|
||||||
ctxSwitchForProcess: contextSwitching
|
|
||||||
? (this._readyQueue[0]?.processId ?? null)
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Inicialização ────────────────────────────────────────────────────────
|
// ─── Inicialização ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
private static _initProcessRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
||||||
* Inicializa o estado de runtime de um processo a partir de sua definição.
|
const isThreaded = (p.threads?.length ?? 0) > 0;
|
||||||
* O primeiro burst deve ser do tipo "cpu"; caso contrário, um erro é lançado.
|
|
||||||
*/
|
if (!isThreaded) {
|
||||||
private static _initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
|
||||||
const firstBurst = p.bursts[0];
|
const firstBurst = p.bursts[0];
|
||||||
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||||
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
processId: p.id,
|
processId: p.id,
|
||||||
state: "New",
|
state: "New",
|
||||||
remainingBurst: firstBurst.duration,
|
remainingBurst: isThreaded ? 1 : (p.bursts[0]?.duration ?? 0),
|
||||||
burstIndex: 0,
|
burstIndex: 0,
|
||||||
arrivalTick: p.arrivalTime,
|
arrivalTick: p.arrivalTime,
|
||||||
startTick: null,
|
startTick: null,
|
||||||
@@ -397,4 +881,68 @@ export class SimulationEngine {
|
|||||||
currentPriority: p.priority,
|
currentPriority: p.priority,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _initThreadStructures(p: Process): void {
|
||||||
|
const processId = p.id;
|
||||||
|
const model = p.threadModel ?? "ONE_TO_ONE";
|
||||||
|
const policy = p.threadSchedulingPolicy ?? "FCFS";
|
||||||
|
const quantum = p.threadQuantum ?? 2;
|
||||||
|
const threads = p.threads!;
|
||||||
|
|
||||||
|
// Criação do mapa de runtimes de threads
|
||||||
|
const threadMap = new Map<string, ThreadRuntime>(
|
||||||
|
threads.map((t) => [
|
||||||
|
t.tid,
|
||||||
|
SimulationEngine._initThreadRuntime(t, processId, p.arrivalTime, quantum),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
this._threadRuntimes.set(processId, threadMap);
|
||||||
|
|
||||||
|
// Escalonador intra-processo
|
||||||
|
this._threadSchedulers.set(processId, new ThreadScheduler(policy, quantum));
|
||||||
|
|
||||||
|
// Estruturas de estado
|
||||||
|
this._activeThreads.set(processId, null);
|
||||||
|
this._threadIoQueues.set(processId, []);
|
||||||
|
this._readyThreadQueues.set(processId, []);
|
||||||
|
|
||||||
|
// MANY_TO_MANY: inicializa slots de kernel
|
||||||
|
if (model === "MANY_TO_MANY") {
|
||||||
|
const M =
|
||||||
|
p.kernelThreadCount ??
|
||||||
|
Math.max(1, Math.ceil(threads.length / 2));
|
||||||
|
this._kernelSlotsAvailable.set(processId, M);
|
||||||
|
this._kernelWaitQueues.set(processId, []);
|
||||||
|
} else {
|
||||||
|
this._kernelWaitQueues.set(processId, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _initThreadRuntime(
|
||||||
|
t: Thread,
|
||||||
|
processId: string,
|
||||||
|
arrivalTick: number,
|
||||||
|
quantum: number,
|
||||||
|
): ThreadRuntime {
|
||||||
|
const firstBurst = t.bursts[0];
|
||||||
|
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||||
|
throw new Error(`Thread ${t.tid}: first burst must be a CPU burst`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
threadId: t.tid,
|
||||||
|
processId,
|
||||||
|
state: "New",
|
||||||
|
remainingBurst: firstBurst.duration,
|
||||||
|
burstIndex: 0,
|
||||||
|
arrivalTick,
|
||||||
|
startTick: null,
|
||||||
|
finishTick: null,
|
||||||
|
waitingTime: 0,
|
||||||
|
cpuTime: 0,
|
||||||
|
ioTime: 0,
|
||||||
|
responseTime: null,
|
||||||
|
quantumRemaining: quantum,
|
||||||
|
currentPriority: t.priority ?? 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import type {
|
import type {
|
||||||
GlobalMetrics,
|
GlobalMetrics,
|
||||||
|
Process,
|
||||||
ProcessMetrics,
|
ProcessMetrics,
|
||||||
ProcessRuntime,
|
ProcessRuntime,
|
||||||
|
Thread,
|
||||||
|
ThreadMetrics,
|
||||||
|
ThreadRuntime,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -83,6 +87,41 @@ export function computeGlobalMetrics(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Thread metrics
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export function computeThreadMetrics(
|
||||||
|
trt: ThreadRuntime,
|
||||||
|
thread: Thread,
|
||||||
|
process: Process,
|
||||||
|
): ThreadMetrics {
|
||||||
|
if (trt.startTick === null || trt.finishTick === null) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot compute metrics for thread ${trt.threadId}: not yet terminated`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnaroundTime = trt.finishTick - trt.arrivalTick;
|
||||||
|
const responseTime = trt.startTick - trt.arrivalTick;
|
||||||
|
const waitingTime = turnaroundTime - trt.cpuTime - trt.ioTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
threadId: trt.threadId,
|
||||||
|
processId: trt.processId,
|
||||||
|
threadName: thread.name,
|
||||||
|
processName: process.name,
|
||||||
|
arrivalTime: trt.arrivalTick,
|
||||||
|
startTick: trt.startTick,
|
||||||
|
finishTick: trt.finishTick,
|
||||||
|
responseTime,
|
||||||
|
turnaroundTime,
|
||||||
|
waitingTime: Math.max(0, waitingTime),
|
||||||
|
cpuTime: trt.cpuTime,
|
||||||
|
ioTime: trt.ioTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Classify process by declared burst ratios (static, pre-simulation)
|
// Classify process by declared burst ratios (static, pre-simulation)
|
||||||
// Used in the process list preview badge.
|
// Used in the process list preview badge.
|
||||||
|
|||||||
+317
-1
@@ -1,4 +1,5 @@
|
|||||||
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
||||||
|
// Thread preset IDs are prefixed with "t_" to avoid conflicts
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -107,7 +108,7 @@ export const presets: Preset[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "context_switch_demo",
|
name: "context_switch_demo",
|
||||||
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch",
|
description: "3 FCFS processes with 2 ticks of overhead — see the red Context Switch cells",
|
||||||
processes: [
|
processes: [
|
||||||
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
|
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
|
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
|
||||||
@@ -127,4 +128,319 @@ export const presets: Preset[] = [
|
|||||||
],
|
],
|
||||||
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
|
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// PRESETS DE THREADS — demonstram os três modelos e conceitos de concorrência
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "many_to_one",
|
||||||
|
description:
|
||||||
|
"MANY_TO_ONE: 2 processos com 3 threads cada. Uma thread em I/O bloqueia todo o processo — observe como o processo fica Waiting enquanto as outras threads ficam ociosas.",
|
||||||
|
processes: [
|
||||||
|
{
|
||||||
|
pid: 1,
|
||||||
|
id: "t_p1",
|
||||||
|
name: "Srv-A",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "MANY_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "RR",
|
||||||
|
threadQuantum: 2,
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p1_t1",
|
||||||
|
name: "A-T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
{ type: "io", duration: 4 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t2",
|
||||||
|
name: "A-T2",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 2 },
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t3",
|
||||||
|
name: "A-T3",
|
||||||
|
bursts: [{ type: "cpu", duration: 4 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pid: 2,
|
||||||
|
id: "t_p2",
|
||||||
|
name: "Srv-B",
|
||||||
|
arrivalTime: 2,
|
||||||
|
priority: 5,
|
||||||
|
color: "#81C784",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "MANY_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p2_t1",
|
||||||
|
name: "B-T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p2_t2",
|
||||||
|
name: "B-T2",
|
||||||
|
bursts: [{ type: "cpu", duration: 5 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "one_to_one",
|
||||||
|
description:
|
||||||
|
"ONE_TO_ONE: mesmo cenário do Many-to-One mas com kernel threads independentes. Threads em I/O não bloqueiam o processo — compare o throughput com o preset many_to_one.",
|
||||||
|
processes: [
|
||||||
|
{
|
||||||
|
pid: 1,
|
||||||
|
id: "t_p1",
|
||||||
|
name: "Srv-A",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "RR",
|
||||||
|
threadQuantum: 2,
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p1_t1",
|
||||||
|
name: "A-T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
{ type: "io", duration: 4 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t2",
|
||||||
|
name: "A-T2",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 2 },
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t3",
|
||||||
|
name: "A-T3",
|
||||||
|
bursts: [{ type: "cpu", duration: 4 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pid: 2,
|
||||||
|
id: "t_p2",
|
||||||
|
name: "Srv-B",
|
||||||
|
arrivalTime: 2,
|
||||||
|
priority: 5,
|
||||||
|
color: "#81C784",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "FCFS",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p2_t1",
|
||||||
|
name: "B-T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p2_t2",
|
||||||
|
name: "B-T2",
|
||||||
|
bursts: [{ type: "cpu", duration: 5 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "many_to_many",
|
||||||
|
description:
|
||||||
|
"MANY_TO_MANY: processo com 4 threads de usuário mas apenas 2 kernel threads (M=2). Observe threads em KernelWait aguardando slot disponível.",
|
||||||
|
processes: [
|
||||||
|
{
|
||||||
|
pid: 1,
|
||||||
|
id: "t_p1",
|
||||||
|
name: "App",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#CE93D8",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "MANY_TO_MANY",
|
||||||
|
kernelThreadCount: 2,
|
||||||
|
threadSchedulingPolicy: "PRIORITY",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p1_t1",
|
||||||
|
name: "Worker1",
|
||||||
|
priority: 1,
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
{ type: "io", duration: 2 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t2",
|
||||||
|
name: "Worker2",
|
||||||
|
priority: 2,
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t3",
|
||||||
|
name: "Worker3",
|
||||||
|
priority: 3,
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 4 },
|
||||||
|
{ type: "io", duration: 1 },
|
||||||
|
{ type: "cpu", duration: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t4",
|
||||||
|
name: "Worker4",
|
||||||
|
priority: 4,
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: defaultConfig({ algorithm: "FCFS" }),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "thread_starvation",
|
||||||
|
description:
|
||||||
|
"Starvation de thread: processo com 3 threads em política Priority. A thread de baixa prioridade (T3) é indefinidamente preterida pelas de alta prioridade.",
|
||||||
|
processes: [
|
||||||
|
{
|
||||||
|
pid: 1,
|
||||||
|
id: "t_p1",
|
||||||
|
name: "App",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#F06292",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "PRIORITY",
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p1_t1",
|
||||||
|
name: "High-1",
|
||||||
|
priority: 1,
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 1 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 1 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t2",
|
||||||
|
name: "High-2",
|
||||||
|
priority: 2,
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 1 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 1 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t3",
|
||||||
|
name: "Low",
|
||||||
|
priority: 10,
|
||||||
|
bursts: [{ type: "cpu", duration: 3 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: defaultConfig({ algorithm: "FCFS" }),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "threads_vs_processes",
|
||||||
|
description:
|
||||||
|
"Threads vs Processos: P1 usa 2 threads ONE_TO_ONE para uma tarefa. P2 faz o mesmo trabalho como processo único. Compare turnaround e throughput.",
|
||||||
|
processes: [
|
||||||
|
{
|
||||||
|
pid: 1,
|
||||||
|
id: "t_p1",
|
||||||
|
name: "Multi-T",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#4FC3F7",
|
||||||
|
bursts: [],
|
||||||
|
threadModel: "ONE_TO_ONE",
|
||||||
|
threadSchedulingPolicy: "RR",
|
||||||
|
threadQuantum: 3,
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
tid: "t_p1_t1",
|
||||||
|
name: "T1",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
{ type: "io", duration: 2 },
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tid: "t_p1_t2",
|
||||||
|
name: "T2",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 2 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pid: 2,
|
||||||
|
id: "t_p2",
|
||||||
|
name: "Single-P",
|
||||||
|
arrivalTime: 0,
|
||||||
|
priority: 5,
|
||||||
|
color: "#FFB74D",
|
||||||
|
bursts: [
|
||||||
|
{ type: "cpu", duration: 5 },
|
||||||
|
{ type: "io", duration: 3 },
|
||||||
|
{ type: "cpu", duration: 5 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// ============================================================
|
||||||
|
// ThreadScheduler — Escalonamento intra-processo de threads
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Responsável por selecionar qual thread executa dentro de um processo
|
||||||
|
// quando ele recebe tempo de CPU. Suporta três políticas:
|
||||||
|
//
|
||||||
|
// FCFS — FIFO estrito por ordem de chegada (arrivalTick)
|
||||||
|
// RR — Round-Robin com quantum intra-processo
|
||||||
|
// PRIORITY — Prioridade da thread (currentPriority, menor = maior prioridade)
|
||||||
|
|
||||||
|
import type { IntraProcessPolicy, ThreadRuntime } from "./types";
|
||||||
|
|
||||||
|
export class ThreadScheduler {
|
||||||
|
readonly policy: IntraProcessPolicy;
|
||||||
|
readonly quantum: number;
|
||||||
|
|
||||||
|
constructor(policy: IntraProcessPolicy = "FCFS", quantum = 2) {
|
||||||
|
this.policy = policy;
|
||||||
|
this.quantum = quantum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seleciona a próxima thread a executar a partir da fila de prontas.
|
||||||
|
* Não modifica o array — o engine remove a thread selecionada.
|
||||||
|
*/
|
||||||
|
select(readyThreads: readonly ThreadRuntime[]): ThreadRuntime | null {
|
||||||
|
if (readyThreads.length === 0) return null;
|
||||||
|
|
||||||
|
switch (this.policy) {
|
||||||
|
case "FCFS":
|
||||||
|
// Primeira a chegar, primeira a ser servida
|
||||||
|
return [...readyThreads].sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.arrivalTick - b.arrivalTick ||
|
||||||
|
a.threadId.localeCompare(b.threadId),
|
||||||
|
)[0] ?? null;
|
||||||
|
|
||||||
|
case "RR":
|
||||||
|
// Ordem de fila (o engine mantém a ordem FIFO ao reinserir)
|
||||||
|
return readyThreads[0] ?? null;
|
||||||
|
|
||||||
|
case "PRIORITY":
|
||||||
|
// Menor currentPriority = maior prioridade; desempate por arrivalTick
|
||||||
|
return [...readyThreads].sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.currentPriority - b.currentPriority ||
|
||||||
|
a.arrivalTick - b.arrivalTick ||
|
||||||
|
a.threadId.localeCompare(b.threadId),
|
||||||
|
)[0] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se o quantum intra-processo da thread expirou.
|
||||||
|
* Apenas relevante para política RR.
|
||||||
|
*/
|
||||||
|
isQuantumExpired(thread: ThreadRuntime): boolean {
|
||||||
|
return this.policy === "RR" && thread.quantumRemaining <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para política PRIORITY: indica se a thread candidata deve preemptar a atual.
|
||||||
|
*/
|
||||||
|
shouldPreemptThread(
|
||||||
|
current: ThreadRuntime,
|
||||||
|
candidate: ThreadRuntime,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
this.policy === "PRIORITY" &&
|
||||||
|
candidate.currentPriority < current.currentPriority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
-2
@@ -11,7 +11,36 @@ export type SchedulingAlgorithm =
|
|||||||
| "PRIORITY_P"
|
| "PRIORITY_P"
|
||||||
| "PRIORITY_RR"
|
| "PRIORITY_RR"
|
||||||
| "MULTILEVEL"
|
| "MULTILEVEL"
|
||||||
| "HRRN";
|
| "HRRN"
|
||||||
|
| "MLFQ";
|
||||||
|
|
||||||
|
// --------------- Thread types --------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modelo de mapeamento threads de usuário → threads de kernel.
|
||||||
|
*
|
||||||
|
* MANY_TO_ONE — N threads de usuário para 1 kernel thread (green threads).
|
||||||
|
* Uma thread em I/O bloqueia todo o processo.
|
||||||
|
* ONE_TO_ONE — 1 thread de usuário para 1 kernel thread (pthreads/POSIX).
|
||||||
|
* Threads bloqueiam independentemente.
|
||||||
|
* MANY_TO_MANY — N threads de usuário para M kernel threads (M ≤ N).
|
||||||
|
* Slots de kernel são liberados durante I/O e reatribuídos.
|
||||||
|
*/
|
||||||
|
export type ThreadModel =
|
||||||
|
| "MANY_TO_ONE"
|
||||||
|
| "ONE_TO_ONE"
|
||||||
|
| "MANY_TO_MANY";
|
||||||
|
|
||||||
|
/** Política de escalonamento intra-processo (entre threads da mesma processo). */
|
||||||
|
export type IntraProcessPolicy = "FCFS" | "RR" | "PRIORITY";
|
||||||
|
|
||||||
|
/** Definição estática de uma thread dentro de um processo. */
|
||||||
|
export interface Thread {
|
||||||
|
tid: string; // identificador único dentro do processo
|
||||||
|
name: string;
|
||||||
|
bursts: BurstSegment[]; // sequência própria de bursts CPU/IO
|
||||||
|
priority?: number; // 1 (mais alta) – 10 (mais baixa); default 5
|
||||||
|
}
|
||||||
|
|
||||||
// --------------- Static process definition ------------------
|
// --------------- Static process definition ------------------
|
||||||
|
|
||||||
@@ -27,7 +56,13 @@ export interface Process {
|
|||||||
arrivalTime: number; // tick of arrival (≥ 0)
|
arrivalTime: number; // tick of arrival (≥ 0)
|
||||||
priority: number; // 1 (highest) – 10 (lowest); default 5
|
priority: number; // 1 (highest) – 10 (lowest); default 5
|
||||||
color: string; // HEX color for Gantt display
|
color: string; // HEX color for Gantt display
|
||||||
bursts: BurstSegment[]; // must start (and end) with a CPU burst
|
bursts: BurstSegment[]; // must start (and end) with a CPU burst; ignored when threads defined
|
||||||
|
// ── Thread fields (optional — backward compatible) ──────
|
||||||
|
threads?: Thread[]; // se definido, bursts acima são ignorados
|
||||||
|
threadModel?: ThreadModel; // default ONE_TO_ONE quando threads definidas
|
||||||
|
threadSchedulingPolicy?: IntraProcessPolicy; // política intra-processo; default FCFS
|
||||||
|
threadQuantum?: number; // quantum para política RR intra-processo; default 2
|
||||||
|
kernelThreadCount?: number; // apenas MANY_TO_MANY: M ≤ threads.length; default ceil(N/2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Scheduler configuration --------------------
|
// --------------- Scheduler configuration --------------------
|
||||||
@@ -39,6 +74,10 @@ export interface MlqQueueDef {
|
|||||||
quantum: number; // used only when algorithm === "RR"
|
quantum: number; // used only when algorithm === "RR"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MlfqLevelDef {
|
||||||
|
quantum: number; // time slice for this level (ticks, >= 1)
|
||||||
|
}
|
||||||
|
|
||||||
export interface SchedulerConfig {
|
export interface SchedulerConfig {
|
||||||
algorithm: SchedulingAlgorithm;
|
algorithm: SchedulingAlgorithm;
|
||||||
quantum: number; // for RR (default 2)
|
quantum: number; // for RR (default 2)
|
||||||
@@ -47,6 +86,8 @@ export interface SchedulerConfig {
|
|||||||
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
||||||
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
||||||
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
|
mlqQueues?: MlqQueueDef[]; // queue definitions for MULTILEVEL algorithm
|
||||||
|
mlfqLevels?: MlfqLevelDef[]; // level definitions for MLFQ algorithm
|
||||||
|
mlfqBoostInterval?: number; // ticks in Ready before boost to level 0; 0 = disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Runtime state ------------------------------
|
// --------------- Runtime state ------------------------------
|
||||||
@@ -58,6 +99,28 @@ export type StateLabel =
|
|||||||
| "Waiting" // executing I/O burst
|
| "Waiting" // executing I/O burst
|
||||||
| "Terminated"; // finished all bursts
|
| "Terminated"; // finished all bursts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado de runtime de uma thread individual.
|
||||||
|
* Espelha ProcessRuntime mas no nível de thread.
|
||||||
|
*/
|
||||||
|
export interface ThreadRuntime {
|
||||||
|
threadId: string;
|
||||||
|
processId: string;
|
||||||
|
state: StateLabel;
|
||||||
|
remainingBurst: number;
|
||||||
|
burstIndex: number;
|
||||||
|
arrivalTick: number; // mesmo que o processo pai
|
||||||
|
startTick: number | null; // primeiro tick em Running
|
||||||
|
finishTick: number | null;
|
||||||
|
waitingTime: number;
|
||||||
|
cpuTime: number;
|
||||||
|
ioTime: number;
|
||||||
|
responseTime: number | null;
|
||||||
|
quantumRemaining: number; // quantum intra-processo (para política RR)
|
||||||
|
currentPriority: number;
|
||||||
|
kernelWaiting?: boolean; // MANY_TO_MANY: aguardando slot de kernel thread
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProcessRuntime {
|
export interface ProcessRuntime {
|
||||||
// ---- spec fields ----
|
// ---- spec fields ----
|
||||||
processId: string;
|
processId: string;
|
||||||
@@ -74,6 +137,7 @@ export interface ProcessRuntime {
|
|||||||
// ---- engine-internal fields ----
|
// ---- engine-internal fields ----
|
||||||
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
|
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
|
||||||
currentPriority: number; // mutable via aging; starts = Process.priority
|
currentPriority: number; // mutable via aging; starts = Process.priority
|
||||||
|
mlfqLevel?: number; // current MLFQ level (0 = highest); undefined for non-MLFQ runs
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Tick snapshot ------------------------------
|
// --------------- Tick snapshot ------------------------------
|
||||||
@@ -86,10 +150,30 @@ export interface SimTick {
|
|||||||
states: Record<string, StateLabel>; // all processId → state
|
states: Record<string, StateLabel>; // all processId → state
|
||||||
contextSwitching: boolean; // true during context-switch overhead
|
contextSwitching: boolean; // true during context-switch overhead
|
||||||
ctxSwitchForProcess: string | null; // incoming processId during ctx-switch overhead (null otherwise)
|
ctxSwitchForProcess: string | null; // incoming processId during ctx-switch overhead (null otherwise)
|
||||||
|
// ── Thread state (only present when at least one process has threads) ──
|
||||||
|
threadStates?: Record<string, Record<string, StateLabel>>; // processId → threadId → state
|
||||||
|
cpuThreads?: Record<string, string | null>; // processId → threadId ativo (null se nenhum)
|
||||||
|
kernelWaitingThreads?: Record<string, string[]>; // MANY_TO_MANY: threadIds em espera de slot
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Metrics ------------------------------------
|
// --------------- Metrics ------------------------------------
|
||||||
|
|
||||||
|
/** Métricas por thread individual (calculadas após terminação). */
|
||||||
|
export interface ThreadMetrics {
|
||||||
|
threadId: string;
|
||||||
|
processId: string;
|
||||||
|
threadName: string;
|
||||||
|
processName?: string;
|
||||||
|
arrivalTime: number;
|
||||||
|
startTick: number;
|
||||||
|
finishTick: number;
|
||||||
|
responseTime: number;
|
||||||
|
turnaroundTime: number;
|
||||||
|
waitingTime: number;
|
||||||
|
cpuTime: number;
|
||||||
|
ioTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProcessMetrics {
|
export interface ProcessMetrics {
|
||||||
processId: string;
|
processId: string;
|
||||||
arrivalTime: number;
|
arrivalTime: number;
|
||||||
|
|||||||
+60
-8
@@ -1,5 +1,4 @@
|
|||||||
import type { ProcessMetrics } from "@core/types";
|
import type { ProcessMetrics, ThreadMetrics, Process } from "@core/types";
|
||||||
import type { Process } from "@core/types";
|
|
||||||
|
|
||||||
interface ProcessMap {
|
interface ProcessMap {
|
||||||
[id: string]: Process;
|
[id: string]: Process;
|
||||||
@@ -16,9 +15,16 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useExport() {
|
export function useExport() {
|
||||||
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
|
const exportCSV = (
|
||||||
const headers = [
|
metrics: ProcessMetrics[],
|
||||||
"Processo",
|
processMap: ProcessMap,
|
||||||
|
threadMetrics?: ThreadMetrics[],
|
||||||
|
) => {
|
||||||
|
// ── Seção de processos ─────────────────────────────────────────────────
|
||||||
|
const processHeaders = [
|
||||||
|
"PID",
|
||||||
|
"Process",
|
||||||
|
"Priority",
|
||||||
"Arrival",
|
"Arrival",
|
||||||
"Start",
|
"Start",
|
||||||
"Finish",
|
"Finish",
|
||||||
@@ -31,10 +37,15 @@ export function useExport() {
|
|||||||
"Bound",
|
"Bound",
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = metrics.map((m) => {
|
const processRows = metrics.map((m) => {
|
||||||
const name = processMap[m.processId]?.name ?? m.processId;
|
const proc = processMap[m.processId];
|
||||||
|
const pid = proc?.pid ?? "";
|
||||||
|
const name = proc?.name ?? m.processId;
|
||||||
|
const priority = proc?.priority ?? "";
|
||||||
return [
|
return [
|
||||||
|
pid,
|
||||||
name,
|
name,
|
||||||
|
priority,
|
||||||
m.arrivalTime,
|
m.arrivalTime,
|
||||||
m.startTick,
|
m.startTick,
|
||||||
m.finishTick,
|
m.finishTick,
|
||||||
@@ -48,7 +59,48 @@ export function useExport() {
|
|||||||
].join(",");
|
].join(",");
|
||||||
});
|
});
|
||||||
|
|
||||||
const csv = [headers.join(","), ...rows].join("\n");
|
const sections: string[] = [
|
||||||
|
"# Processes",
|
||||||
|
processHeaders.join(","),
|
||||||
|
...processRows,
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Seção de threads (opcional) ────────────────────────────────────────
|
||||||
|
if (threadMetrics && threadMetrics.length > 0) {
|
||||||
|
const threadHeaders = [
|
||||||
|
"Thread",
|
||||||
|
"Process",
|
||||||
|
"Arrival",
|
||||||
|
"Start",
|
||||||
|
"Finish",
|
||||||
|
"Response",
|
||||||
|
"Turnaround",
|
||||||
|
"Waiting",
|
||||||
|
"CPU Time",
|
||||||
|
"I/O Time",
|
||||||
|
];
|
||||||
|
|
||||||
|
const threadRows = threadMetrics.map((t) => {
|
||||||
|
const proc = processMap[t.processId];
|
||||||
|
const procName = proc?.name ?? t.processId;
|
||||||
|
return [
|
||||||
|
t.threadName,
|
||||||
|
procName,
|
||||||
|
t.arrivalTime,
|
||||||
|
t.startTick,
|
||||||
|
t.finishTick,
|
||||||
|
t.responseTime,
|
||||||
|
t.turnaroundTime,
|
||||||
|
t.waitingTime,
|
||||||
|
t.cpuTime,
|
||||||
|
t.ioTime,
|
||||||
|
].join(",");
|
||||||
|
});
|
||||||
|
|
||||||
|
sections.push("", "# Threads", threadHeaders.join(","), ...threadRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csv = sections.join("\n");
|
||||||
downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
|
downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user