Compare commits

19 Commits
Author SHA1 Message Date
Lucas RayanandClaude Sonnet 5.5 6f061e16bc ci: o ci chamado pelo release herda os secrets
CI / Tipos e testes (push) Successful in 59s
CI / Imagem Docker (linux/arm64) (push) Successful in 1m12s
CI / Trivy (dependências e segredos) (push) Successful in 9s
Release / Plano (reaproveitar a imagem?) (push) Successful in 4s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 31s
Release / Cria a release no Gitea (push) Successful in 6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:52:58 -03:00
Lucas RayanandClaude Sonnet 5.5 b43bd5e37a fix(docker): corrige as vulnerabilidades da imagem final (apk upgrade; sem npm no runtime)
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
CI / Trivy (dependências e segredos) (push) Successful in 6s
CI / Tipos e testes (push) Successful in 1m0s
CI / Imagem Docker (linux/arm64) (push) Failing after 37s
Release / ci (push) Failing after 1m44s
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:49:49 -03:00
Lucas RayanandClaude Sonnet 5.5 522805c826 ci: login no registro só antes de publicar
CI / Tipos e testes (push) Successful in 1m0s
CI / Trivy (dependências e segredos) (push) Successful in 6s
CI / Imagem Docker (linux/arm64) (push) Failing after 51s
Release / ci (push) Failing after 2m0s
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Release / Plano (reaproveitar a imagem?) (push) Successful in 5s
Build, Trivy e smoke test não precisam de credencial.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:46:19 -03:00
Lucas RayanandClaude Sonnet 5.5 4ea17cbac2 ci: volta ao secret REGISTRY_TOKEN para publicar no registro
Release / Plano (reaproveitar a imagem?) (push) Successful in 4s
CI / Tipos e testes (push) Successful in 1m0s
CI / Trivy (dependências e segredos) (push) Successful in 7s
CI / Imagem Docker (linux/arm64) (push) Failing after 6s
Release / ci (push) Failing after 1m13s
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
O registro do Gitea recusa o token do job (unauthorized).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:44:15 -03:00
Lucas RayanandClaude Sonnet 5.5 3645c6cbc8 ci: autentica no registro com o token do job, sem secret
Release / Plano (reaproveitar a imagem?) (push) Successful in 4s
CI / Tipos e testes (push) Successful in 57s
CI / Trivy (dependências e segredos) (push) Successful in 6s
CI / Imagem Docker (linux/arm64) (push) Failing after 7s
Release / ci (push) Failing after 1m15s
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:41:44 -03:00
Lucas RayanandClaude Sonnet 5.5 450537ea7e fix(docker): imagem final sem privilégios (nginx-unprivileged, porta 8080)
Release / Plano (reaproveitar a imagem?) (push) Successful in 5s
CI / Tipos e testes (push) Successful in 1m1s
CI / Trivy (dependências e segredos) (push) Successful in 7s
CI / Imagem Docker (linux/arm64) (push) Failing after 7s
Release / ci (push) Failing after 1m20s
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:39:30 -03:00
Lucas RayanandClaude Sonnet 5.5 f1ab23ab14 fix(ci): scripts executáveis
Release / Plano (reaproveitar a imagem?) (push) Successful in 5s
CI / Tipos e testes (push) Successful in 1m2s
CI / Trivy (dependências e segredos) (push) Failing after 6s
Release / ci (push) Failing after 1m9s
CI / Imagem Docker (linux/arm64) (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:36:07 -03:00
Lucas RayanandClaude Sonnet 5.5 ab7f532456 ci: imagem construída do código, esteira para os runners do Gitea e changelog
Release / Plano (reaproveitar a imagem?) (push) Failing after 4s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 21:35:00 -03:00
Lucas Rayan 47586bcc7b fix: update healthcheck command to use CMD-SHELL for improved compatibility 2026-07-10 16:50:12 -03:00
Lucas Rayan Guerra 393ce8bee1 Update healthcheck URL in docker-compose.yml 2026-07-10 16:43:49 -03:00
Lucas Rayan Guerra 1ef1429466 Update healthcheck and port mapping in docker-compose 2026-07-10 16:42:36 -03:00
Lucas Rayan Guerra ba8265f820 Remove port mapping for service 'pss' 2026-07-10 16:33:38 -03:00
Lucas Rayan Guerra 6630aedc75 Update healthcheck command and start period 2026-07-10 16:28:22 -03:00
Lucas Rayan Guerra 578c746771 Add healthcheck configuration to docker-compose
Add healthcheck to ensure service availability
2026-07-10 16:19:00 -03:00
Lucas RayanandClaude Sonnet 4.6 b707e4990c chore: bump version label in Sidebar to v1.3.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:08:54 -03:00
Lucas Rayan 2ee3eeb2b6 feat: update version to 1.3.2, add regression tests for RR scheduling with threads 2026-03-25 10:04:52 -03:00
Lucas Rayan 44ee62e2c2 feat: update version to 1.3.1, enhance GanttChart and MetricsPanel, and add ThreadMetricsChart 2026-03-25 09:59:47 -03:00
Lucas Guerra 5761dfe70a feat: add MLFQ scheduling algorithm and thread scheduler
- Implemented MLFQ (Multilevel Feedback Queue) algorithm with dynamic priority levels and quantum management.
- Created unit tests for MLFQ algorithm covering initialization, demotion, selection, priority boost, preemption, and quantum expiration.
- Developed a ThreadScheduler class supporting FCFS, RR, and PRIORITY scheduling policies for intra-process thread management.
- Added unit tests for ThreadScheduler to validate thread selection and quantum expiration logic.
2026-03-24 23:09:16 -03:00
Lucas RayanandClaude Sonnet 4.6 532c14e382 chore: add Zenodo DOI to CITATION.cff
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 20:10:16 -03:00
51 changed files with 4866 additions and 387 deletions

No files matched your search

+10 -2
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+119
View File
@@ -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"
+129
View File
@@ -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
View File
@@ -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
+79
View File
@@ -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
+6 -1
View File
@@ -6,7 +6,12 @@ version: 1.2.0
date-released: "2026-03-23" date-released: "2026-03-23"
license: MIT license: MIT
url: "https://pss.lucasrguerra.dev.br" url: "https://pss.lucasrguerra.dev.br"
repository-code: "https://github.com/lucasrguerra/pss" 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: authors:
- family-names: Guerra - family-names: Guerra
given-names: Lucas Rayan given-names: Lucas Rayan
+4 -4
View File
@@ -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
View File
@@ -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;"]
+60 -9
View File
@@ -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.
@@ -83,8 +110,10 @@ docker compose up
| **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) |
| **Priority RR**: Priority with Round Robin | ✅ | Quantum + Aging (optional) |
| **HRRN**: Highest Response Ratio Next | ❌ | None | | **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.
@@ -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
@@ -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).
+60 -9
View File
@@ -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.
@@ -81,8 +108,10 @@ docker compose up
| **Round Robin (RR)** | ✅ | Quantum | | **Round Robin (RR)** | ✅ | Quantum |
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) | | **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) | | **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
| **Priority RR**: Prioridade com Round Robin | ✅ | Quantum + Aging (opcional) |
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum | | **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.
@@ -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
@@ -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
View File
Binary file not shown.
+7 -1
View File
@@ -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
+100
View File
@@ -158,6 +158,106 @@ This allows different classes of processes to be treated distinctly: for example
--- ---
## 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.
+100
View File
@@ -158,6 +158,106 @@ Isso permite tratar diferentes classes de processos de forma distinta: por exemp
--- ---
## 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 eventual­mente 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.
+35
View File
@@ -105,6 +105,41 @@ 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:
+35
View File
@@ -105,6 +105,41 @@ 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:
+1 -1
View File
@@ -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;
+19 -11
View File
@@ -1,12 +1,12 @@
{ {
"name": "pss", "name": "pss",
"version": "1.2.0", "version": "1.3.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pss", "name": "pss",
"version": "1.2.0", "version": "1.3.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -147,6 +147,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -519,6 +520,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
@@ -567,6 +569,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
} }
@@ -1892,8 +1895,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
@@ -2034,6 +2036,7 @@
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
@@ -2051,6 +2054,7 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -2062,6 +2066,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
@@ -2373,6 +2378,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2725,8 +2731,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
@@ -3544,7 +3549,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"lz-string": "bin/bin.js" "lz-string": "bin/bin.js"
} }
@@ -3760,6 +3764,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3802,7 +3807,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1", "ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0", "ansi-styles": "^5.0.0",
@@ -3818,7 +3822,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3829,7 +3832,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -3852,6 +3854,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -3864,6 +3867,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -3884,6 +3888,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@@ -3960,7 +3965,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@@ -4546,6 +4552,7 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -4644,6 +4651,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/chai": "^5.2.2", "@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4", "@vitest/expect": "3.2.4",
+4 -5
View File
@@ -1,12 +1,11 @@
{ {
"name": "pss", "name": "pss",
"version": "1.2.0", "version": "1.3.3",
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser", "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"
} }
+21
View File
@@ -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"
+39
View File
@@ -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
+33
View File
@@ -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 ]
+1 -1
View File
@@ -66,7 +66,7 @@ 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="Simulation controls" aria-label="Simulation controls"
> >
+60 -39
View File
@@ -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"
style={{ minHeight: 80 }}
>
Start the simulation to see the Gantt chart 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,34 +60,64 @@ 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) */}
<div
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 }}
>
<span className="text-[10px] font-bold truncate" style={{ color: process.color }}>
{process.name}
</span>
</div>
<GanttRow return (
process={process} <div key={process.id}>
ticks={ticks} {/* ── Linha do processo ─────────────────────────── */}
onCellMouseEnter={setTooltip} <div
onCellMouseLeave={handleMouseLeave} 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"
style={{ width: 60, height: CELL_H }}
>
<span
className="text-[10px] font-bold truncate"
style={{ color: process.color }}
>
{process.name}
</span>
</div>
<GanttRow
process={process}
ticks={ticks}
onCellMouseEnter={setTooltip}
onCellMouseLeave={handleMouseLeave}
/>
</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;
+55 -17
View File
@@ -86,23 +86,61 @@ const AppHeader = () => {
Preset Scenarios Preset Scenarios
</div> </div>
<div className="overflow-y-auto"> <div className="overflow-y-auto">
{presets.map((preset, i) => ( {/* Classic process presets */}
<button <div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
key={preset.name} Processes
onClick={() => handleLoadPreset(i)} </div>
className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group" {presets
> .filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
<div className="text-xs font-medium text-slate-200 group-hover:text-white"> .map((preset) => {
{preset.name} const i = presets.indexOf(preset);
</div> return (
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug"> <button
{preset.description} key={preset.name}
</div> onClick={() => handleLoadPreset(i)}
<div className="text-[10px] text-blue-400 mt-0.5"> className="w-full text-left px-3 py-2.5 hover:bg-slate-700 transition-colors group"
{preset.config.algorithm} >
</div> <div className="text-xs font-medium text-slate-200 group-hover:text-white">
</button> {preset.name}
))} </div>
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
{preset.description}
</div>
<div className="text-[10px] text-blue-400 mt-0.5">
{preset.config.algorithm}
</div>
</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>
</> </>
+1 -1
View File
@@ -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 />
+1 -7
View File
@@ -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-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
Process Scheduler 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.2.0 PSS v1.3.3
</div> </div>
</aside> </aside>
); );
+34 -16
View File
@@ -28,6 +28,22 @@ 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 (
@@ -80,10 +96,9 @@ 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);
const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1); const maxCpu = Math.max(...metrics.map(m => m.cpuTime), 1);
const maxIo = Math.max(...metrics.map(m => m.ioTime), 1); const maxIo = Math.max(...metrics.map(m => m.ioTime), 1);
@@ -98,11 +113,11 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
metrics.forEach(m => { metrics.forEach(m => {
const key = getName(m.processId); const key = getName(m.processId);
switch (dim) { switch (dim) {
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;
} }
}); });
return row; return row;
@@ -110,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">
Comparative Profile (normalized) Comparative Profile (normalized)
</p> </p>
<ResponsiveContainer width="100%" height={400}> <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
@@ -142,16 +160,16 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
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 per Process (ticks) Breakdown per Process (ticks)
</p> </p>
<ResponsiveContainer width="100%" height={400}> <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
@@ -183,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>
+41 -14
View File
@@ -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' | 'charts'; type Tab = 'table' | 'global' | 'charts' | 'threads' | 'thread-charts';
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'table', label: 'Per Process', icon: <Table size={13} /> },
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
];
const MetricsPanel = () => { const MetricsPanel = () => {
const [activeTab, setActiveTab] = useState<Tab>('table'); const [activeTab, setActiveTab] = useState<Tab>('table');
@@ -36,20 +32,40 @@ const MetricsPanel = () => {
); );
} }
// 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: '600px' }}> <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,12 +78,17 @@ 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 ? 'Available when simulation completes' : 'Export metrics as 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"
@@ -80,7 +101,7 @@ 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} />
)} )}
@@ -90,6 +111,12 @@ const MetricsPanel = () => {
{activeTab === 'charts' && ( {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>
); );
@@ -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;
+33 -5
View File
@@ -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>
<Badge variant={boundVariant}>{bound}</Badge> {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>
)}
</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>
)} )}
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</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>
)}
</div> </div>
</div> </div>
+192 -66
View File
@@ -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('Name is required'); if (!name.trim()) errs.push('Name is required');
if (name.trim().length > 8) errs.push('Name must be at most 8 characters'); if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0'); if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10'); if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
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 (hasThreads) {
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be 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}: duration must be ≥ 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} and ${i + 1} cannot be the same type (alternate 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();
@@ -118,7 +208,6 @@ 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">Name</label> <label className={labelCls} htmlFor="pf-name">Name</label>
<input <input
@@ -132,7 +221,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
aria-label="Process name" aria-label="Process name"
/> />
</div> </div>
{/* Color */}
<div> <div>
<label className={labelCls} htmlFor="pf-color">Color</label> <label className={labelCls} htmlFor="pf-color">Color</label>
<input <input
@@ -146,7 +234,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
</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
@@ -160,7 +247,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
/> />
</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
@@ -176,51 +262,91 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
</div> </div>
</div> </div>
{/* Bursts editor */} {/* Toggle threads */}
<div> <div className="flex items-center justify-between border-t border-slate-700 pt-3">
<div className="flex items-center justify-between mb-2"> <div>
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label> <span className="text-xs text-slate-300 font-medium">Use Threads</span>
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}> <p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
<Plus size={13} />
</IconButton>
</div>
<div className="space-y-1.5" data-testid="burst-list">
{bursts.map((burst, idx) => (
<div
key={idx}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
burst.type === 'cpu'
? 'border-emerald-700/50 bg-emerald-900/20'
: 'border-cyan-700/50 bg-cyan-900/20'
}`}
>
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
{burst.type}
</span>
<input
type="number"
min={1}
value={burst.duration}
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"
aria-label={`Burst ${idx + 1} duration`}
/>
<span className="text-[10px] text-slate-500">ticks</span>
<IconButton
size="sm"
variant="ghost"
label={`Remove burst ${idx + 1}`}
onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1}
className="hover:text-red-400"
>
<Minus size={12} />
</IconButton>
</div>
))}
</div> </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> </div>
{/* Tabs: Bursts / Threads */}
{!hasThreads ? (
/* ── Editor de bursts (original) ─────────────────────────── */
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
<Plus size={13} />
</IconButton>
</div>
<div className="space-y-1.5" data-testid="burst-list">
{bursts.map((burst, idx) => (
<div
key={idx}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
burst.type === 'cpu'
? 'border-emerald-700/50 bg-emerald-900/20'
: 'border-cyan-700/50 bg-cyan-900/20'
}`}
>
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
{burst.type}
</span>
<input
type="number"
min={1}
value={burst.duration}
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"
aria-label={`Burst ${idx + 1} duration`}
/>
<span className="text-[10px] text-slate-500">ticks</span>
<IconButton
size="sm"
variant="ghost"
label={`Remove burst ${idx + 1}`}
onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1}
className="hover:text-red-400"
>
<Minus size={12} />
</IconButton>
</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 && (
<ul className="space-y-0.5" role="alert" aria-live="polite"> <ul className="space-y-0.5" role="alert" aria-live="polite">
@@ -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 (Non-preemptive)' }, { value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' }, { value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' }, { 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 = () => {
@@ -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';
@@ -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);
@@ -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>
); );
}; };
+61
View File
@@ -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)}`;
}
+690
View File
@@ -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");
});
});
+396
View File
@@ -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);
});
});
+129
View File
@@ -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);
});
});
+12
View File
@@ -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;
}
} }
+2
View File
@@ -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(),
}; };
+114
View File
@@ -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;
}
}
+685 -137
View File
File diff suppressed because it is too large. Load diff
+39
View File
@@ -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.
+316
View File
@@ -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;
@@ -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 }),
},
]; ];
+74
View File
@@ -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
);
}
}
+90 -6
View File
@@ -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,17 +86,41 @@ 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 ------------------------------
export type StateLabel = export type StateLabel =
| "New" // before arrivalTime | "New" // before arrivalTime
| "Ready" // in ready queue, waiting for CPU | "Ready" // in ready queue, waiting for CPU
| "Running" // executing CPU burst | "Running" // executing CPU burst
| "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;
+51 -6
View File
@@ -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,8 +15,13 @@ 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[],
processMap: ProcessMap,
threadMetrics?: ThreadMetrics[],
) => {
// ── Seção de processos ─────────────────────────────────────────────────
const processHeaders = [
"PID", "PID",
"Process", "Process",
"Priority", "Priority",
@@ -33,7 +37,7 @@ export function useExport() {
"Bound", "Bound",
]; ];
const rows = metrics.map((m) => { const processRows = metrics.map((m) => {
const proc = processMap[m.processId]; const proc = processMap[m.processId];
const pid = proc?.pid ?? ""; const pid = proc?.pid ?? "";
const name = proc?.name ?? m.processId; const name = proc?.name ?? m.processId;
@@ -55,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;");
}; };