Compare commits
19
Commits
v1.2.0
...
6f061e16bc
No files matched your search
+10
-2
@@ -1,5 +1,13 @@
|
||||
node_modules
|
||||
dist/
|
||||
dist
|
||||
dist.tar.gz
|
||||
.git
|
||||
.gitignore
|
||||
.gitea
|
||||
.gitattributes
|
||||
.claude
|
||||
.wolf
|
||||
.superset
|
||||
docs
|
||||
*.md
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,119 @@
|
||||
name: CI
|
||||
|
||||
# Documentação e configuração de ferramenta não mudam nada que a suíte teste.
|
||||
# Na main não roda por push: o release.yml é quem a chama, e só quando a imagem
|
||||
# precisa ser reconstruída; rodar nos dois repetiria a mesma suíte no mesmo commit.
|
||||
# Sem gatilho de pull_request: o push da branch já valida o mesmo commit, e os
|
||||
# dois juntos rodariam tudo duas vezes (o repositório não recebe PR de fork).
|
||||
#
|
||||
# A imagem é construída aqui uma única vez, em arm64 (a arquitetura do servidor),
|
||||
# testada e publicada como inputs-<hash>. Ao fazer o merge, o release encontra
|
||||
# essa tag e só aplica os nomes novos, sem rodar nem construir nada de novo.
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [main]
|
||||
paths-ignore: ['**.md', 'LICENSE', 'CITATION.cff', 'paper.md', 'references.bib', 'docs/**', '.claude/**', '.wolf/**', '.superset/**', 'scripts/release-notes.sh']
|
||||
schedule:
|
||||
- cron: '0 9 * * 1' # toda segunda: reavalia com a base de vulnerabilidades nova
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verificar:
|
||||
name: Tipos e testes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
|
||||
# tsc -b é o mesmo que o build faz antes do Vite; aqui falha antes de gastar a imagem.
|
||||
- run: npx tsc -b
|
||||
- run: npm test
|
||||
|
||||
# Trivy roda pela imagem oficial: o trivy-action clona o repositório do Trivy
|
||||
# do mesmo servidor do workflow, que aqui é o Gitea, e não o GitHub.
|
||||
# O workspace do job é um volume que o daemon não enxerga, então os arquivos
|
||||
# entram no container com `docker cp` em vez de `-v`.
|
||||
seguranca:
|
||||
name: Trivy (dependências e segredos)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Repositório
|
||||
run: |
|
||||
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
|
||||
fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 .)
|
||||
docker cp . "$cid:/src"
|
||||
docker start -a "$cid"
|
||||
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||
docker rm "$cid" >/dev/null
|
||||
exit "$status"
|
||||
|
||||
# Só começa depois das verificações baratas: um teste quebrado não gasta um
|
||||
# build de imagem. Roda no runner arm64, o mesmo tipo do servidor, então o
|
||||
# que é escaneado e testado é a imagem que vai ao ar.
|
||||
imagem:
|
||||
name: Imagem Docker (linux/arm64)
|
||||
needs: [verificar, seguranca]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
env:
|
||||
REGISTRY: git.lucasrguerra.dev.br
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Conteúdo já publicado (re-push, tag num commit já validado): nada a fazer.
|
||||
- id: plano
|
||||
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# `docker build` simples, no daemon persistente do runner: as camadas e o
|
||||
# cache mount do npm sobrevivem entre execuções. Um buildx criaria um
|
||||
# BuildKit novo, e vazio, a cada job.
|
||||
- name: Build
|
||||
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||
run: docker build -t pss:ci .
|
||||
|
||||
- name: Trivy na imagem
|
||||
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||
run: |
|
||||
docker run --rm --network host -v trivy-cache:/root/.cache/ -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \
|
||||
image --severity HIGH,CRITICAL --exit-code 1 pss:ci
|
||||
|
||||
# A SPA sobe, cai no index nas rotas internas e entrega os assets com
|
||||
# cache imutável (scripts/smoke-imagem.sh).
|
||||
- name: A imagem sobe e responde
|
||||
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||
run: |
|
||||
docker run -d --name pss-ci -p 127.0.0.1:3998:8080 pss:ci
|
||||
status=0
|
||||
scripts/smoke-imagem.sh http://127.0.0.1:3998 || status=1
|
||||
docker rm -f pss-ci >/dev/null
|
||||
exit "$status"
|
||||
|
||||
# O login fica por último: build, Trivy e smoke não precisam de credencial.
|
||||
- if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
# Passou em tudo: publica a mesma imagem que foi testada, sem reconstruir.
|
||||
- name: Publica inputs-<hash>
|
||||
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||
run: |
|
||||
tag="${{ steps.plano.outputs.image }}:inputs-${{ steps.plano.outputs.inputs }}"
|
||||
docker tag pss:ci "$tag"
|
||||
docker push "$tag"
|
||||
@@ -0,0 +1,129 @@
|
||||
name: Release
|
||||
|
||||
# Publica a imagem no registro de containers do Gitea depois de um merge em main,
|
||||
# e de novo com tag semver quando uma tag v* é enviada. O servidor só faz pull.
|
||||
# Precisa do secret REGISTRY_TOKEN no repositório (token do Gitea com write:package):
|
||||
# o registro recusa o token do job.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: git.lucasrguerra.dev.br
|
||||
|
||||
jobs:
|
||||
# A imagem só depende dos arquivos que entram no build, não da documentação nem
|
||||
# da configuração de ferramentas. O push da branch já a construiu, testou e
|
||||
# publicou como inputs-<hash>; quando essa tag existe (o caso normal de um
|
||||
# merge, uma tag num commit validado, um merge só de docs), não há o que
|
||||
# construir: as tags novas apontam para o manifesto existente.
|
||||
plano:
|
||||
name: Plano (reaproveitar a imagem?)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
inputs: ${{ steps.plano.outputs.inputs }}
|
||||
reuse: ${{ steps.plano.outputs.reuse }}
|
||||
image: ${{ steps.plano.outputs.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: plano
|
||||
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Nada é publicado sem a suíte completa passar antes. Pulada quando a imagem é
|
||||
# reaproveitada: o mesmo conteúdo já passou por ela.
|
||||
ci:
|
||||
needs: plano
|
||||
if: ${{ needs.plano.outputs.reuse != 'true' }}
|
||||
uses: ./.gitea/workflows/ci.yml
|
||||
# Workflows reutilizáveis não herdam os secrets sozinhos: o login no registro precisa deles.
|
||||
secrets: inherit
|
||||
|
||||
publicar:
|
||||
name: Publica a imagem (linux/arm64)
|
||||
needs: [plano, ci]
|
||||
# ci é pulado, e não bem-sucedido, quando a imagem é reaproveitada.
|
||||
if: ${{ !cancelled() && needs.plano.result == 'success' && (needs.ci.result == 'success' || needs.plano.outputs.reuse == 'true') }}
|
||||
# O único runner é arm64 e o servidor de destino (Oracle) também, então a
|
||||
# imagem é construída nativamente, sem emulação.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Se o ci chamado acima acabou de publicar a imagem (push direto na main),
|
||||
# o plano lá de cima está desatualizado: confere de novo antes de construir.
|
||||
- id: replano
|
||||
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Autentica no registro
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Metadados
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ needs.plano.outputs.image }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
type=sha,format=long
|
||||
|
||||
- name: Build e push
|
||||
if: ${{ steps.replano.outputs.reuse != 'true' }}
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tags }}
|
||||
${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ needs.plano.outputs.image }}:buildcache
|
||||
cache-to: type=registry,ref=${{ needs.plano.outputs.image }}:buildcache,mode=max
|
||||
|
||||
# Mesmo manifesto, nomes novos: nenhuma camada é lida nem escrita.
|
||||
- name: Aplica as tags à imagem existente
|
||||
if: ${{ steps.replano.outputs.reuse == 'true' }}
|
||||
run: |
|
||||
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
||||
docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}"
|
||||
|
||||
# Cria a entrada em "Releases" quando a execução é de uma tag v*. Roda depois
|
||||
# de publicar a imagem, com as notas da seção do CHANGELOG.md.
|
||||
release:
|
||||
name: Cria a release no Gitea
|
||||
needs: [plano, publicar]
|
||||
# Explícito, como o publicar: o ci acima é pulado quando a imagem é
|
||||
# reaproveitada, e o success() implícito pularia este job também.
|
||||
if: ${{ !cancelled() && needs.publicar.result == 'success' && startsWith(gitea.ref, 'refs/tags/v') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Release
|
||||
env:
|
||||
TAG: ${{ gitea.ref_name }}
|
||||
TOKEN: ${{ gitea.token }}
|
||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||
IMAGE: ${{ needs.plano.outputs.image }}
|
||||
run: |
|
||||
scripts/release-notes.sh "$TAG" "$IMAGE" > notes.md
|
||||
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
||||
jq -n --arg tag "$TAG" --arg name "PSS ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
||||
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||
-H 'Content-Type: application/json' -d @release.json
|
||||
+7
-16
@@ -1,19 +1,10 @@
|
||||
!**/glob-import/dir/node_modules
|
||||
.DS_Store
|
||||
.idea
|
||||
.pnpm-store
|
||||
*.cpuprofile
|
||||
*.local
|
||||
*.log
|
||||
/.vscode/
|
||||
/docs/.vitepress/cache
|
||||
/docs/.vitepress/.temp
|
||||
/packages/vite/LICENSE
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
explorations
|
||||
node_modules
|
||||
playground-temp
|
||||
temp
|
||||
TODOs.md
|
||||
dist.tar.gz
|
||||
*.local
|
||||
*.log
|
||||
.DS_Store
|
||||
.idea
|
||||
/.vscode/
|
||||
.eslintcache
|
||||
@@ -0,0 +1,79 @@
|
||||
# Changelog
|
||||
|
||||
Everything notable in PSS is recorded here. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows
|
||||
[Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.3.3] - 2026-11-01
|
||||
|
||||
### Changed
|
||||
|
||||
- The code, the container image and the releases now live on Gitea
|
||||
(`git.lucasrguerra.dev.br`). The deployment pulls the image from its registry.
|
||||
- The `Dockerfile` is multi-stage: it builds Vite from source inside the image instead
|
||||
of unpacking a `dist.tar.gz` committed to the repository (removed, along with the
|
||||
`build:release` script). The final stage is `nginx-unprivileged`: the container runs
|
||||
as the `nginx` user and listens on port 8080 instead of 80 (Trivy DS-0002).
|
||||
- New CI on Gitea runners (`.gitea/workflows`): types, 282 tests, Trivy on the repository
|
||||
and on the image, and an image smoke test. The image is built once per content hash and
|
||||
reused by the release, so a merge or a tag on an already validated commit rebuilds nothing.
|
||||
- Repository links in `package.json`, `CITATION.cff`, the READMEs and `CONTRIBUTING.md`
|
||||
point to Gitea. `.gitattributes` forces LF line endings.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `docker-compose.yml` healthcheck uses `CMD-SHELL` and the correct local URL, and the
|
||||
port mapping honours `PORT`.
|
||||
|
||||
## [1.3.2] - 2026-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- Regression tests for Round Robin scheduling with threads.
|
||||
|
||||
## [1.3.1] - 2026-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- `ThreadMetricsChart` and per-thread metrics.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improvements to the Gantt chart and the metrics panel.
|
||||
|
||||
## [1.3.0] - 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- Multilevel Feedback Queue (MLFQ) scheduling algorithm.
|
||||
- Thread scheduler.
|
||||
|
||||
## [1.2.0] - 2026-03-23
|
||||
|
||||
### Added
|
||||
|
||||
- English documentation, a `CONTRIBUTING.md`, the JOSS paper and a Zenodo DOI in
|
||||
`CITATION.cff`.
|
||||
- Multilevel Queue (MLQ) scheduling algorithm and its documentation.
|
||||
|
||||
### Changed
|
||||
|
||||
- License switched to MIT.
|
||||
- Scheduling algorithms refactored into a class-based `BaseAlgorithm` structure, with
|
||||
the engine and tests updated and new algorithm and metrics documentation.
|
||||
|
||||
## [1.0.0] - 2026-03-23
|
||||
|
||||
### Added
|
||||
|
||||
- First release of the browser-based CPU scheduling simulator: core scheduling engine,
|
||||
algorithms (including Priority Round Robin), Gantt chart with context-switch handling,
|
||||
UI components and Docker setup.
|
||||
|
||||
[Unreleased]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.3.3...HEAD
|
||||
[1.3.3]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.3.2...v1.3.3
|
||||
[1.3.2]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.2.0...v1.3.2
|
||||
[1.2.0]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/compare/v1.0.0...v1.2.0
|
||||
[1.0.0]: https://git.lucasrguerra.dev.br/lucasrguerra/pss/releases/tag/v1.0.0
|
||||
+6
-1
@@ -6,7 +6,12 @@ version: 1.2.0
|
||||
date-released: "2026-03-23"
|
||||
license: MIT
|
||||
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:
|
||||
- family-names: Guerra
|
||||
given-names: Lucas Rayan
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ This document provides guidelines and instructions for contributing to this proj
|
||||
|
||||
This section guides you through submitting a bug report for PSS. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||
|
||||
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.
|
||||
- Exact steps to reproduce the problem.
|
||||
- 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
|
||||
|
||||
1. **Fork the repository** on GitHub.
|
||||
1. **Fork the repository** on Gitea.
|
||||
2. **Clone your fork** locally:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/pss.git
|
||||
git clone https://git.lucasrguerra.dev.br/YOUR_USERNAME/pss.git
|
||||
cd pss
|
||||
```
|
||||
3. **Install dependencies**:
|
||||
@@ -78,7 +78,7 @@ If you add a new feature, please include appropriate tests covering the new func
|
||||
```bash
|
||||
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
|
||||
git push origin feature/my-awesome-feature
|
||||
```
|
||||
|
||||
+19
-8
@@ -1,13 +1,24 @@
|
||||
# ── Production image ──
|
||||
FROM nginx:alpine
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ── 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 --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
# Unpack pre-built Vite output
|
||||
COPY dist.tar.gz /usr/share/nginx/html/
|
||||
RUN tar -xzf /usr/share/nginx/html/dist.tar.gz --strip-components=1 -C /usr/share/nginx/html \
|
||||
&& rm /usr/share/nginx/html/dist.tar.gz
|
||||
|
||||
EXPOSE 80
|
||||
USER nginx
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||
CMD wget -q --spider http://127.0.0.1:8080/ || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
+60
-9
@@ -35,7 +35,7 @@ A live demo is available at: **https://pss.lucasrguerra.dev.br/**
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/lucasrguerra/pss.git
|
||||
git clone https://git.lucasrguerra.dev.br/lucasrguerra/pss.git
|
||||
cd pss
|
||||
|
||||
# Install dependencies
|
||||
@@ -57,17 +57,44 @@ npm run preview
|
||||
### With Docker
|
||||
|
||||
```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
|
||||
|
||||
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.
|
||||
2. **Configure the scheduler**: Choose an algorithm and adjust the parameters (quantum for RR, aging for Priority, etc.).
|
||||
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, 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×).
|
||||
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.
|
||||
|
||||
@@ -83,8 +110,10 @@ docker compose up
|
||||
| **Round Robin (RR)** | ✅ | Quantum |
|
||||
| **Priority NP**: Non-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 |
|
||||
| **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.
|
||||
|
||||
@@ -119,20 +148,30 @@ src/
|
||||
│ │ ├── priority_rr.ts
|
||||
│ │ ├── hrrn.ts
|
||||
│ │ ├── multilevel.ts
|
||||
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||
│ │ └── index.ts # Dispatch by algorithm
|
||||
│ ├── engine.ts # SimulationEngine, orchestrates the ticks
|
||||
│ ├── metrics.ts # Per-process and global metrics computation
|
||||
│ ├── engine.ts # SimulationEngine, orchestrates the ticks (thread-aware)
|
||||
│ ├── thread-scheduler.ts # Intra-process scheduler (FCFS / RR / Priority)
|
||||
│ ├── metrics.ts # Per-process, global, and per-thread metrics computation
|
||||
│ ├── types.ts # All TypeScript interfaces
|
||||
│ └── presets.ts # Pre-defined scenarios
|
||||
│ └── presets.ts # Pre-defined scenarios (processes and threads)
|
||||
│
|
||||
├── store/ # Global state with Zustand
|
||||
├── components/ # React components
|
||||
│ ├── ProcessPanel/ # Process creation and editing
|
||||
│ │ ├── ThreadEditor.tsx # Thread editor (model, policy, per-thread bursts)
|
||||
│ │ └── ...
|
||||
│ ├── SchedulerPanel/ # Algorithm selection and parameters
|
||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, speed
|
||||
│ ├── GanttChart/ # Interactive Gantt chart
|
||||
│ │ ├── GanttThreadRow.tsx # Thread sub-row in the Gantt chart
|
||||
│ │ └── ...
|
||||
│ ├── MetricsPanel/ # Table, charts, and global metrics
|
||||
│ │ ├── ThreadMetricsTable.tsx # Per-thread metrics table
|
||||
│ │ └── ...
|
||||
│ └── shared/ # Buttons, badges, tooltips, modals
|
||||
│ ├── threadUtils.ts # Thread color derivation (HSL)
|
||||
│ └── ...
|
||||
│
|
||||
└── hooks/ # Custom hooks (animation loop, export, etc.)
|
||||
```
|
||||
@@ -143,6 +182,8 @@ src/
|
||||
|
||||
Load classic scenarios with a single click to explore OS phenomena:
|
||||
|
||||
**Processes:**
|
||||
|
||||
| Preset | What it demonstrates |
|
||||
|--------|----------------------|
|
||||
| `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 |
|
||||
| `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
|
||||
@@ -205,4 +256,4 @@ Contributions are welcome! Please read our [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
## 📜 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).
|
||||
@@ -33,7 +33,7 @@ Ideal para:
|
||||
|
||||
```bash
|
||||
# Clone o repositório
|
||||
git clone https://github.com/lucasrguerra/pss.git
|
||||
git clone https://git.lucasrguerra.dev.br/lucasrguerra/pss.git
|
||||
cd pss
|
||||
|
||||
# Instale as dependências
|
||||
@@ -55,17 +55,44 @@ npm run preview
|
||||
### Com Docker
|
||||
|
||||
```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
|
||||
|
||||
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.
|
||||
2. **Configure o escalonador**: Escolha o algoritmo e ajuste os parâmetros (quantum para RR, aging para Priority, etc.).
|
||||
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, 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×).
|
||||
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.
|
||||
|
||||
@@ -81,8 +108,10 @@ docker compose up
|
||||
| **Round Robin (RR)** | ✅ | Quantum |
|
||||
| **Priority NP**: Prioridade Não-Preemptiva | ❌ | Aging (opcional) |
|
||||
| **Priority P**: Prioridade Preemptiva | ✅ | Aging (opcional) |
|
||||
| **Priority RR**: Prioridade com Round Robin | ✅ | Quantum + Aging (opcional) |
|
||||
| **HRRN**: Highest Response Ratio Next | ❌ | Nenhum |
|
||||
| **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.
|
||||
|
||||
@@ -117,20 +146,30 @@ src/
|
||||
│ │ ├── priority_rr.ts
|
||||
│ │ ├── hrrn.ts
|
||||
│ │ ├── multilevel.ts
|
||||
│ │ ├── mlfq.ts # Multilevel Feedback Queue
|
||||
│ │ └── index.ts # Dispatch por algoritmo
|
||||
│ ├── engine.ts # SimulationEngine, orquestra os ticks
|
||||
│ ├── metrics.ts # Cálculo de métricas por processo e globais
|
||||
│ ├── engine.ts # SimulationEngine, orquestra os ticks (suporte a threads)
|
||||
│ ├── 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
|
||||
│ └── presets.ts # Cenários pré-definidos
|
||||
│ └── presets.ts # Cenários pré-definidos (processos e threads)
|
||||
│
|
||||
├── store/ # Estado global com Zustand
|
||||
├── components/ # Componentes React
|
||||
│ ├── 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
|
||||
│ ├── ControlBar/ # Play, Pause, Step, Reset, velocidade
|
||||
│ ├── GanttChart/ # Diagrama de Gantt interativo
|
||||
│ │ ├── GanttThreadRow.tsx # Sub-linha de thread no Gantt
|
||||
│ │ └── ...
|
||||
│ ├── MetricsPanel/ # Tabela, gráficos e métricas globais
|
||||
│ │ ├── ThreadMetricsTable.tsx # Tabela de métricas por thread
|
||||
│ │ └── ...
|
||||
│ └── shared/ # Botões, badges, tooltips, modais
|
||||
│ ├── threadUtils.ts # Derivação de cores de threads (HSL)
|
||||
│ └── ...
|
||||
│
|
||||
└── 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:
|
||||
|
||||
**Processos:**
|
||||
|
||||
| Preset | O que demonstra |
|
||||
|--------|-----------------|
|
||||
| `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 |
|
||||
| `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
|
||||
@@ -203,4 +254,4 @@ Contribuições são bem-vindas! Por favor, consulte nosso guia em [CONTRIBUTING
|
||||
|
||||
## 📜 Licença
|
||||
|
||||
Distribuído sob a [Licença MIT](./LICENSE). Desenvolvido por [Lucas Rayan Guerra](https://github.com/lucasrguerra).
|
||||
Distribuído sob a [Licença MIT](./LICENSE). Desenvolvido por [Lucas Rayan Guerra](https://git.lucasrguerra.dev.br/lucasrguerra).
|
||||
BIN
Binary file not shown.
+7
-1
@@ -2,5 +2,11 @@ services:
|
||||
pss:
|
||||
build: .
|
||||
ports:
|
||||
- "${PORT:-3000}:80"
|
||||
- "${PORT:-3000}:8080"
|
||||
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
|
||||
@@ -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
|
||||
|
||||
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10th edition.
|
||||
|
||||
@@ -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 eventualmente retornam ao topo.
|
||||
|
||||
#### Exemplo com 3 filas (quantuns: 2, 4, 8)
|
||||
|
||||
```
|
||||
Processo A (CPU Bound, burst=12):
|
||||
Fila 0 → usa 2 ticks, rebaixado → Fila 1
|
||||
Fila 1 → usa 4 ticks, rebaixado → Fila 2
|
||||
Fila 2 → usa 6 ticks restantes (quantum=8, não esgota)
|
||||
|
||||
Processo B (I/O Bound, burst=1 CPU + I/O + burst=1 CPU):
|
||||
Fila 0 → usa 1 tick → vai para I/O → retorna à Fila 0
|
||||
→ sempre permanece na fila de maior prioridade
|
||||
```
|
||||
|
||||
### Parâmetros
|
||||
|
||||
| Parâmetro | Padrão | Descrição |
|
||||
|-----------|--------|-----------|
|
||||
| Número de filas | 3 | Quantidade de filas de prioridade |
|
||||
| Quantuns por fila | [2, 4, 8] | Fatia de tempo de cada fila |
|
||||
| Intervalo de boost | 20 ticks | A cada N ticks, todos os processos sobem para a fila 0 |
|
||||
|
||||
---
|
||||
|
||||
## Modelos de Threads
|
||||
|
||||
O PSS suporta simulação de **threads** dentro de processos, implementando os três modelos clássicos descritos em Sistemas Operacionais.
|
||||
|
||||
### Conceitos Gerais
|
||||
|
||||
- Um processo pode ter **N threads de usuário**, cada uma com sua própria sequência de bursts (CPU e I/O).
|
||||
- A política de escalonamento entre threads de um mesmo processo é configurável: **FCFS**, **Round Robin** ou **Prioridade**.
|
||||
- O modelo de thread determina como threads de usuário se mapeiam para threads de kernel.
|
||||
|
||||
### MANY_TO_ONE (N:1)
|
||||
|
||||
**N threads de usuário → 1 thread de kernel**
|
||||
|
||||
- Apenas uma thread pode usar a CPU por vez dentro do processo.
|
||||
- Se qualquer thread iniciar I/O, **todo o processo bloqueia** (a thread de kernel fica presa).
|
||||
- Sem paralelismo real; muito simples de implementar.
|
||||
- Simulado pelos presets `many_to_one`.
|
||||
|
||||
### ONE_TO_ONE (1:1)
|
||||
|
||||
**Cada thread de usuário → 1 thread de kernel dedicada**
|
||||
|
||||
- Threads bloqueiam **independentemente**: se a Thread A faz I/O, as Threads B e C continuam na fila Ready.
|
||||
- Processo só termina quando **todas** as threads terminam.
|
||||
- Processo bloqueia somente quando todas as threads estão simultâneamente em I/O.
|
||||
- Simulado pelos presets `one_to_one`.
|
||||
|
||||
### MANY_TO_MANY (N:M)
|
||||
|
||||
**N threads de usuário → M threads de kernel (M ≤ N)**
|
||||
|
||||
- O número de threads de kernel (`kernelThreadCount`) é configurável.
|
||||
- Se todas as M slots de kernel estiverem ocupadas, threads de usuário adicionais ficam em **fila de espera de kernel** (`KernelWait`).
|
||||
- Combina flexibilidade com controle de recursos: mais threads ativas que N:1 mas menos overhead que 1:1.
|
||||
- Simulado pelos presets `many_to_many`.
|
||||
|
||||
### Política Intra-Processo
|
||||
|
||||
Define como o escalonador seleciona a próxima thread quando há múltiplas threads Ready no mesmo processo:
|
||||
|
||||
| Política | Comportamento |
|
||||
|----------|--------------|
|
||||
| **FCFS** | Thread mais antiga na fila Ready é selecionada |
|
||||
| **Round Robin (RR)** | Cada thread recebe `threadQuantum` ticks; ao esgotar, vai para o fim da fila |
|
||||
| **Priority** | Thread de maior prioridade é selecionada; preempção imediata se nova thread de maior prioridade chegar |
|
||||
|
||||
### Diagrama de Gantt com Threads
|
||||
|
||||
Quando um processo possui threads, o Gantt exibe **sub-linhas** abaixo da linha do processo, uma por thread, com cores derivadas da cor do processo (deslocamento de matiz de 40° por thread). O estado de cada thread (New, Ready, Running, Waiting, Terminated) é colorido com as mesmas variáveis CSS dos processos.
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Silberschatz, A., Galvin, P. B., & Gagne, G., *Operating System Concepts*, 10ª edição.
|
||||
|
||||
@@ -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
|
||||
|
||||
Consider three processes with the following bursts:
|
||||
|
||||
@@ -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
|
||||
|
||||
Considere três processos com os seguintes bursts:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
Generated
+19
-11
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pss",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pss",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
@@ -147,6 +147,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -519,6 +520,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -567,6 +569,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -1892,8 +1895,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -2034,6 +2036,7 @@
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -2051,6 +2054,7 @@
|
||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2062,6 +2066,7 @@
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
@@ -2373,6 +2378,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -2725,8 +2731,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
@@ -3544,7 +3549,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -3760,6 +3764,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3802,7 +3807,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -3818,7 +3822,6 @@
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3829,7 +3832,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3852,6 +3854,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -3864,6 +3867,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -3884,6 +3888,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -3960,7 +3965,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -4546,6 +4552,7 @@
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -4644,6 +4651,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
|
||||
+4
-5
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "pss",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.3",
|
||||
"description": "Process Scheduler Simulator — an interactive educational tool that runs entirely in the browser",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:release": "tsc -b && vite build && tar --dereference -czf dist.tar.gz dist",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
@@ -39,14 +38,14 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lucasrguerra/pss.git"
|
||||
"url": "git+https://git.lucasrguerra.dev.br/lucasrguerra/pss.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Lucas Rayan Guerra",
|
||||
"license": "MIT",
|
||||
"type": "commonjs",
|
||||
"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"
|
||||
}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uso: scripts/plano-imagem.sh <registro> <repositório>
|
||||
# Imprime inputs=, image= e reuse= (para $GITHUB_OUTPUT). O hash cobre só os
|
||||
# arquivos que entram no build da imagem, não a documentação nem a configuração
|
||||
# de ferramentas. reuse=true quando o registro já tem inputs-<hash>.
|
||||
set -euo pipefail
|
||||
registry=$1; ns=${2,,}
|
||||
|
||||
inputs=$(git ls-tree -r HEAD \
|
||||
| awk -F'\t' '$2 !~ /^(\.gitea|\.github|\.claude|\.wolf|\.superset|docs)\// && $2 !~ /^(README.*|CONTRIBUTING\.md|CHANGELOG\.md|CITATION\.cff|LICENSE|paper\.md|references\.bib|\.gitignore|\.gitattributes|scripts\/release-notes\.sh)$/' \
|
||||
| sha256sum | cut -c1-16)
|
||||
token=$(curl -fsS "https://$registry/v2/token?service=container_registry&scope=repository:$ns:pull" | jq -r .token)
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -I \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json' \
|
||||
"https://$registry/v2/$ns/manifests/inputs-$inputs")
|
||||
echo "inputs-$inputs -> HTTP $code" >&2
|
||||
reuse=false; [ "$code" = 200 ] && reuse=true
|
||||
echo "inputs=$inputs"
|
||||
echo "image=$registry/$ns"
|
||||
echo "reuse=$reuse"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Imprime as notas de uma release: a imagem publicada (quando houver) e a seção
|
||||
# do CHANGELOG.md daquela versão (ou o log de commits desde a tag anterior, se
|
||||
# não houver seção).
|
||||
#
|
||||
# scripts/release-notes.sh v1.2.3 [git.exemplo.com/dono/projeto]
|
||||
set -euo pipefail
|
||||
|
||||
tag=${1:?uso: release-notes.sh <tag> [imagem]}
|
||||
image=${2:-}
|
||||
version=${tag#v}
|
||||
|
||||
if [ -n "$image" ]; then
|
||||
echo "## Imagem"
|
||||
echo
|
||||
echo '```'
|
||||
echo "$image:$version"
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
|
||||
section=""
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
section=$(awk -v head="## [$version]" '
|
||||
index($0, head) == 1 { found = 1; next }
|
||||
found && /^## \[/ { exit }
|
||||
found && /^\[[^]]+\]: / { next }
|
||||
found { print }
|
||||
' CHANGELOG.md)
|
||||
fi
|
||||
|
||||
if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
|
||||
printf '%s\n' "$section"
|
||||
else
|
||||
previous=$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)
|
||||
echo "## Mudanças${previous:+ desde $previous}"
|
||||
echo
|
||||
git log --no-merges --format='- %s (%h)' "${previous:+$previous..}$tag"
|
||||
fi
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Teste de fumaça da imagem: a SPA serve o index, cai no index em rotas internas
|
||||
# e entrega os assets com cache longo.
|
||||
#
|
||||
# scripts/smoke-imagem.sh http://127.0.0.1:3998
|
||||
#
|
||||
# Cada falha é impressa e o script sai com 1 no fim, para mostrar tudo de uma vez.
|
||||
set -uo pipefail
|
||||
|
||||
base=${1:?uso: smoke-imagem.sh <url-base>}
|
||||
falhas=0
|
||||
falhou() { echo "FALHOU $1"; falhas=$((falhas + 1)); }
|
||||
|
||||
# O nginx leva um instante para subir.
|
||||
for _ in $(seq 1 30); do curl -fsS -o /dev/null "$base/" 2>/dev/null && break; sleep 1; done
|
||||
|
||||
index=$(curl -fsS "$base/") || { echo "FALHOU /: sem resposta"; exit 1; }
|
||||
grep -q 'id="root"' <<< "$index" || falhou "/: o index não tem o #root"
|
||||
|
||||
# Rota interna da SPA devolve o mesmo index (try_files), não 404.
|
||||
[ "$(curl -s -o /dev/null -w '%{http_code}' "$base/qualquer/rota")" = 200 ] || falhou "/qualquer/rota: esperado 200"
|
||||
|
||||
# Todo asset que o index referencia existe e tem cache imutável.
|
||||
assets=$(grep -oE '(src|href)="/assets/[^"]+"' <<< "$index" | cut -d'"' -f2)
|
||||
[ -n "$assets" ] || falhou "o index não referencia nenhum /assets/"
|
||||
for a in $assets; do
|
||||
head=$(curl -sI "$base$a")
|
||||
grep -q '^HTTP/[0-9.]* 200' <<< "$head" || falhou "$a: esperado 200"
|
||||
grep -qi 'cache-control:.*immutable' <<< "$head" || falhou "$a: sem cache imutável"
|
||||
done
|
||||
|
||||
[ "$falhas" = 0 ] && echo "ok: $(wc -w <<< "$assets") assets e rotas conferidos"
|
||||
[ "$falhas" = 0 ]
|
||||
@@ -66,7 +66,7 @@ const ControlBar = () => {
|
||||
|
||||
return (
|
||||
<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"
|
||||
aria-label="Simulation controls"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSimulationStore } from "@store/simulationStore";
|
||||
import { useProcessStore } from "@store/processStore";
|
||||
import { useGanttScroll } from "@hooks/useGanttScroll";
|
||||
import GanttRow from "./GanttRow";
|
||||
import GanttThreadRow from "./GanttThreadRow";
|
||||
import GanttLegend from "./GanttLegend";
|
||||
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
|
||||
import { CELL_H, CELL_W } from "./constants";
|
||||
@@ -13,33 +14,29 @@ const GanttChart = () => {
|
||||
const status = useSimulationStore((s) => s.status);
|
||||
const processes = useProcessStore((s) => s.processes);
|
||||
|
||||
// Scrollable container ref for auto-scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const currentTickIndex = ticks.length - 1;
|
||||
useGanttScroll(scrollRef, currentTickIndex, status === "running");
|
||||
|
||||
// Tooltip state
|
||||
const [tooltip, setTooltip] = useState<TooltipData | null>(null);
|
||||
const handleMouseLeave = useCallback(() => setTooltip(null), []);
|
||||
|
||||
// Empty state
|
||||
if (ticks.length === 0) {
|
||||
return (
|
||||
<div 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 }}>
|
||||
<div
|
||||
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
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── ruler (tick numbers) ──────────────────────────────────
|
||||
const tickNumbers = ticks.map((t) => t.tick);
|
||||
const totalWidth = ticks.length * CELL_W;
|
||||
|
||||
return (
|
||||
<div className="border-b border-slate-800 bg-slate-950 flex flex-col">
|
||||
|
||||
{/* ── scrollable grid + cursor ── */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="overflow-x-auto overflow-y-hidden gantt-scrollbar relative"
|
||||
@@ -48,9 +45,10 @@ const GanttChart = () => {
|
||||
|
||||
{/* Tick ruler */}
|
||||
<div className="flex sticky top-0 z-10 bg-slate-950 border-b border-slate-800">
|
||||
{/* Spacer for sticky process labels column */}
|
||||
<div className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0" style={{ width: 48, height: 16 }} />
|
||||
|
||||
<div
|
||||
className="sticky left-0 z-20 bg-slate-950 border-r border-slate-700 shrink-0"
|
||||
style={{ width: 60, height: 16 }}
|
||||
/>
|
||||
{tickNumbers.map((t) => (
|
||||
<div
|
||||
key={t}
|
||||
@@ -62,16 +60,25 @@ const GanttChart = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Process rows */}
|
||||
{/* Process rows (+ thread sub-rows) */}
|
||||
<div className="relative">
|
||||
{processes.map((process) => (
|
||||
<div key={process.id} className="flex items-center border-b border-white/5 last:border-b-0">
|
||||
{/* Process label (sticky on the left) */}
|
||||
{processes.map((process) => {
|
||||
const hasThreads = (process.threads?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div key={process.id}>
|
||||
{/* ── Linha do processo ─────────────────────────── */}
|
||||
<div
|
||||
className={`flex items-center border-b border-white/5 ${hasThreads ? 'border-b-0' : ''}`}
|
||||
>
|
||||
<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 }}
|
||||
style={{ width: 60, height: CELL_H }}
|
||||
>
|
||||
<span
|
||||
className="text-[10px] font-bold truncate"
|
||||
style={{ color: process.color }}
|
||||
>
|
||||
<span className="text-[10px] font-bold truncate" style={{ color: process.color }}>
|
||||
{process.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -83,13 +90,34 @@ const GanttChart = () => {
|
||||
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 */}
|
||||
{currentTickIndex >= 0 && (
|
||||
<div
|
||||
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>
|
||||
@@ -97,14 +125,7 @@ const GanttChart = () => {
|
||||
</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 />
|
||||
|
||||
{/* ── tooltip ── */}
|
||||
{tooltip && <GanttTooltip data={tooltip} />}
|
||||
</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;
|
||||
@@ -86,7 +86,15 @@ const AppHeader = () => {
|
||||
Preset Scenarios
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
{presets.map((preset, i) => (
|
||||
{/* Classic process presets */}
|
||||
<div className="px-3 py-1 text-[9px] text-slate-500 uppercase tracking-wider font-semibold bg-slate-800/80 sticky top-0">
|
||||
Processes
|
||||
</div>
|
||||
{presets
|
||||
.filter(p => !['many_to_one','one_to_one','many_to_many','thread_starvation','threads_vs_processes'].includes(p.name))
|
||||
.map((preset) => {
|
||||
const i = presets.indexOf(preset);
|
||||
return (
|
||||
<button
|
||||
key={preset.name}
|
||||
onClick={() => handleLoadPreset(i)}
|
||||
@@ -102,7 +110,37 @@ const AppHeader = () => {
|
||||
{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>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import MetricsPanel from '../MetricsPanel/MetricsPanel';
|
||||
|
||||
const MainArea = () => {
|
||||
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 />
|
||||
<GanttChart />
|
||||
<MetricsPanel />
|
||||
|
||||
@@ -4,12 +4,6 @@ import SchedulerPanel from '../SchedulerPanel/SchedulerPanel';
|
||||
const Sidebar = () => {
|
||||
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">
|
||||
<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">
|
||||
<ProcessPanel />
|
||||
<div className="border-t border-slate-700 pt-4">
|
||||
@@ -18,7 +12,7 @@ const Sidebar = () => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,22 @@ const TOOLTIP_STYLE = {
|
||||
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) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
@@ -80,7 +96,6 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
|
||||
// ── Radar chart data (normalized 0-100 per 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 maxR = Math.max(...metrics.map(m => m.responseTime), 1);
|
||||
const maxW = Math.max(...metrics.map(m => m.waitingTime), 1);
|
||||
@@ -110,15 +125,18 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
|
||||
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 (
|
||||
<div className={`p-3 flex gap-3 ${showRadar ? 'flex-row' : 'flex-col'}`}>
|
||||
<div className="p-3 flex flex-col gap-6">
|
||||
{/* ── Radar chart ───────────────────────────────────────── */}
|
||||
{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">
|
||||
Comparative Profile (normalized)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartBox height={360}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
@@ -142,16 +160,16 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 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">
|
||||
Breakdown per Process (ticks)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartBox height={360}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
@@ -183,7 +201,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
<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>
|
||||
</ResponsiveContainer>
|
||||
</ChartBox>
|
||||
<p className="text-center text-[9px] text-slate-600 mt-0.5">
|
||||
Waiting + CPU + I/O = Turnaround
|
||||
</p>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { Download, BarChart2, Table, Globe } from 'lucide-react';
|
||||
import { computeProcessMetrics, computeGlobalMetrics } from '@core/metrics';
|
||||
import { Download, BarChart2, Table, Globe, Layers } from 'lucide-react';
|
||||
import { computeProcessMetrics, computeGlobalMetrics, computeThreadMetrics } from '@core/metrics';
|
||||
import { useSimulationStore } from '../../store/simulationStore';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import { useExport } from '../../hooks/useExport';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import GlobalMetrics from './GlobalMetrics';
|
||||
import MetricsChart from './MetricsChart';
|
||||
import ThreadMetricsTable from './ThreadMetricsTable';
|
||||
import ThreadMetricsChart from './ThreadMetricsChart';
|
||||
|
||||
type Tab = 'table' | 'global' | '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} /> },
|
||||
];
|
||||
type Tab = 'table' | 'global' | 'charts' | 'threads' | 'thread-charts';
|
||||
|
||||
const MetricsPanel = () => {
|
||||
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 completedRuntimes = runtimes.filter(rt => rt.finishTick !== null);
|
||||
const processMetrics = completedRuntimes.map(rt => computeProcessMetrics(rt));
|
||||
const globalMetrics = computeGlobalMetrics(processMetrics, ticks.length);
|
||||
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 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 (
|
||||
<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 */}
|
||||
<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
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
@@ -62,12 +78,17 @@ const MetricsPanel = () => {
|
||||
>
|
||||
{tab.icon}
|
||||
{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>
|
||||
))}
|
||||
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={() => exportCSV(processMetrics, processMap)}
|
||||
onClick={() => exportCSV(processMetrics, processMap, hasThreads ? threadMetrics : undefined)}
|
||||
disabled={!canExportCSV}
|
||||
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"
|
||||
@@ -80,7 +101,7 @@ const MetricsPanel = () => {
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div>
|
||||
{activeTab === 'table' && (
|
||||
<MetricsTable metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
@@ -90,6 +111,12 @@ const MetricsPanel = () => {
|
||||
{activeTab === 'charts' && (
|
||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'threads' && (
|
||||
<ThreadMetricsTable metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
{activeTab === 'thread-charts' && (
|
||||
<ThreadMetricsChart metrics={threadMetrics} processMap={processMap} />
|
||||
)}
|
||||
</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;
|
||||
@@ -10,10 +10,28 @@ interface ProcessCardProps {
|
||||
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 bound = classifyBound(process.bursts);
|
||||
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 hasThreads = (process.threads?.length ?? 0) > 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';
|
||||
|
||||
@@ -38,7 +56,13 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
</span>
|
||||
)}
|
||||
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
|
||||
{hasThreads ? (
|
||||
<span className="text-[9px] font-semibold px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
|
||||
{MODEL_LABEL[process.threadModel ?? 'ONE_TO_ONE']} · {process.threads!.length}T
|
||||
</span>
|
||||
) : (
|
||||
<Badge variant={boundVariant}>{bound}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
@@ -47,7 +71,11 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
{totalIo > 0 && (
|
||||
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
|
||||
)}
|
||||
{hasThreads ? (
|
||||
<span>Threads: <span className="text-slate-300 font-medium">{process.threads!.length}</span></span>
|
||||
) : (
|
||||
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useState, useEffect } from '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 IconButton from '../shared/IconButton';
|
||||
import { generateId, nextColor } from '../shared/processUtils';
|
||||
import ThreadEditor from './ThreadEditor';
|
||||
|
||||
interface ProcessFormProps {
|
||||
/** If provided, the form edits this process; otherwise creates a new one. */
|
||||
editingProcess?: Process | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type FormTab = 'bursts' | 'threads';
|
||||
|
||||
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
|
||||
|
||||
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
@@ -19,12 +21,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const isEditing = !!editingProcess;
|
||||
|
||||
// ── Campos base ─────────────────────────────────────────────────────────
|
||||
const [name, setName] = useState('');
|
||||
const [arrivalTime, setArrivalTime] = useState(0);
|
||||
const [priority, setPriority] = useState(5);
|
||||
const [color, setColor] = useState(nextColor);
|
||||
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
|
||||
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(() => {
|
||||
if (editingProcess) {
|
||||
@@ -33,24 +45,38 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
setPriority(editingProcess.priority);
|
||||
setColor(editingProcess.color);
|
||||
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 {
|
||||
setName('');
|
||||
setArrivalTime(0);
|
||||
setPriority(5);
|
||||
setColor(nextColor());
|
||||
setBursts([{ type: 'cpu', duration: 4 }]);
|
||||
setHasThreads(false);
|
||||
setThreads([]);
|
||||
setThreadModel('ONE_TO_ONE');
|
||||
setThreadPolicy('FCFS');
|
||||
setThreadQuantum(2);
|
||||
setKernelThreadCount(2);
|
||||
setActiveTab('bursts');
|
||||
}
|
||||
setErrors([]);
|
||||
}, [editingProcess]);
|
||||
|
||||
// ---- Burst helpers ----
|
||||
|
||||
// ── Helpers de bursts ───────────────────────────────────────────────────
|
||||
const updateBurstDuration = (index: number, value: number) => {
|
||||
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
|
||||
};
|
||||
|
||||
const addBurst = () => {
|
||||
// enforce alternating cpu/io pattern
|
||||
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
|
||||
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
|
||||
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
|
||||
@@ -58,36 +84,90 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const removeBurst = (index: number) => {
|
||||
if (bursts.length <= 1) return;
|
||||
setBursts(prev => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
// Safe-guard: ensure it still starts (and ends) with cpu
|
||||
return next;
|
||||
});
|
||||
setBursts(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// ---- 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 errs: string[] = [];
|
||||
if (!name.trim()) errs.push('Name is required');
|
||||
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 (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
||||
|
||||
if (hasThreads) {
|
||||
if (threads.length === 0) errs.push('At least one thread is required');
|
||||
threads.forEach((t, ti) => {
|
||||
if (!t.name.trim()) errs.push(`Thread ${ti + 1}: name is required`);
|
||||
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 (alternate CPU/IO)`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type)
|
||||
errs.push(`Bursts ${i} and ${i + 1} cannot be the same type`);
|
||||
});
|
||||
}
|
||||
|
||||
setErrors(errs);
|
||||
return errs.length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
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) {
|
||||
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
|
||||
updateProcess(editingProcess.id, processData);
|
||||
} else {
|
||||
addProcess({
|
||||
id: generateId(),
|
||||
@@ -95,7 +175,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
arrivalTime,
|
||||
priority,
|
||||
color,
|
||||
bursts,
|
||||
bursts: hasThreads ? [] : bursts,
|
||||
...(hasThreads
|
||||
? {
|
||||
threads,
|
||||
threadModel,
|
||||
threadSchedulingPolicy: threadPolicy,
|
||||
threadQuantum,
|
||||
kernelThreadCount:
|
||||
threadModel === 'MANY_TO_MANY' ? kernelThreadCount : undefined,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
@@ -118,7 +208,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
{/* Basic fields */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 flex gap-3">
|
||||
{/* Name */}
|
||||
<div className="flex-1">
|
||||
<label className={labelCls} htmlFor="pf-name">Name</label>
|
||||
<input
|
||||
@@ -132,7 +221,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
aria-label="Process name"
|
||||
/>
|
||||
</div>
|
||||
{/* Color */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-color">Color</label>
|
||||
<input
|
||||
@@ -146,7 +234,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrival */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
|
||||
<input
|
||||
@@ -160,7 +247,6 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
|
||||
<input
|
||||
@@ -176,7 +262,31 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bursts editor */}
|
||||
{/* Toggle threads */}
|
||||
<div className="flex items-center justify-between border-t border-slate-700 pt-3">
|
||||
<div>
|
||||
<span className="text-xs text-slate-300 font-medium">Use Threads</span>
|
||||
<p className="text-[10px] text-slate-500">Substitui bursts por threads com execução própria</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={hasThreads}
|
||||
onClick={() => toggleThreads(!hasThreads)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
hasThreads ? 'bg-blue-600' : 'bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||
hasThreads ? 'translate-x-4' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs: Bursts / Threads */}
|
||||
{!hasThreads ? (
|
||||
/* ── Editor de bursts (original) ─────────────────────────── */
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||
@@ -220,6 +330,22 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
))}
|
||||
</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 */}
|
||||
{errors.length > 0 && (
|
||||
|
||||
@@ -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 }[] = [
|
||||
{ value: 'FCFS', label: 'FCFS — First Come First Served' },
|
||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
|
||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
|
||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (Non-preemptive)' },
|
||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (Preemptive)' },
|
||||
{ value: 'RR', label: 'RR — Round Robin' },
|
||||
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
||||
{ 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 = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
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
|
||||
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']);
|
||||
|
||||
// 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 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 config = useProcessStore(s => s.config);
|
||||
const setConfig = useProcessStore(s => s.setConfig);
|
||||
@@ -250,6 +339,16 @@ const SchedulerConfig = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Deriva uma cor para uma thread a partir da cor base do processo.
|
||||
* Desloca o matiz (hue) em 35° por índice de thread para garantir
|
||||
* cores visualmente distintas dentro do mesmo processo.
|
||||
*/
|
||||
export function deriveThreadColor(processColor: string, threadIndex: number): string {
|
||||
const [h, s, l] = hexToHsl(processColor);
|
||||
const newH = (h + threadIndex * 40) % 360;
|
||||
const newL = Math.min(80, Math.max(35, l + (threadIndex % 2 === 0 ? 8 : -8)));
|
||||
return hslToHex(newH, Math.min(100, s + 10), newL);
|
||||
}
|
||||
|
||||
function hexToHsl(hex: string): [number, number, number] {
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
h /= 360; s /= 100; l /= 100;
|
||||
let r: number, g: number, b: number;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
const toHex = (x: number) =>
|
||||
Math.round(x * 255).toString(16).padStart(2, "0");
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { Process, SchedulerConfig } from "../types";
|
||||
|
||||
const BASE_CONFIG: SchedulerConfig = {
|
||||
algorithm: "FCFS",
|
||||
quantum: 10,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
};
|
||||
|
||||
function makeThreadedProcess(
|
||||
overrides: Partial<Process> = {},
|
||||
): Process {
|
||||
return {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "P1",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "T1",
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
{
|
||||
tid: "t2",
|
||||
name: "T2",
|
||||
bursts: [{ type: "cpu", duration: 2 }],
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ── ONE_TO_ONE ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Engine — ONE_TO_ONE básico", () => {
|
||||
it("processo termina quando todas as threads terminam", () => {
|
||||
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
expect(engine.isFinished).toBe(true);
|
||||
const rt = engine.runtimeStates[0]!;
|
||||
expect(rt.state).toBe("Terminated");
|
||||
expect(rt.finishTick).not.toBeNull();
|
||||
});
|
||||
|
||||
it("runtimes de threads são gerados corretamente", () => {
|
||||
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
const threadRts = engine.threadRuntimeStates;
|
||||
expect(threadRts).toHaveLength(2);
|
||||
for (const trt of threadRts) {
|
||||
expect(trt.state).toBe("Terminated");
|
||||
expect(trt.cpuTime).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("cpuTime total do processo = soma dos cpuTimes das threads", () => {
|
||||
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
const pRt = engine.runtimeStates[0]!;
|
||||
const threadTotal = engine.threadRuntimeStates.reduce(
|
||||
(sum, trt) => sum + trt.cpuTime,
|
||||
0,
|
||||
);
|
||||
expect(pRt.cpuTime).toBe(threadTotal);
|
||||
});
|
||||
|
||||
it("processHasThreads retorna true para processo com threads", () => {
|
||||
const p = makeThreadedProcess();
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
expect(engine.processHasThreads("p1")).toBe(true);
|
||||
});
|
||||
|
||||
it("processo sem threads não é afetado", () => {
|
||||
const p: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "P1",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#aaa",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
};
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
expect(engine.isFinished).toBe(true);
|
||||
expect(engine.processHasThreads("p1")).toBe(false);
|
||||
});
|
||||
|
||||
it("ONE_TO_ONE: processo com thread em I/O pode ter outra thread rodando", () => {
|
||||
const p = makeThreadedProcess({
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t2",
|
||||
name: "T2",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const ticks = engine.runAll();
|
||||
|
||||
// O processo NÃO deve ir para Waiting enquanto T2 está ready/running
|
||||
const waitingTicks = ticks.filter(
|
||||
(tick) => tick.states["p1"] === "Waiting",
|
||||
);
|
||||
// Pode ter algum tick Waiting quando AMBAS as threads estão em I/O,
|
||||
// mas aqui T2 não tem I/O, então o processo nunca deve ficar Waiting
|
||||
expect(waitingTicks.length).toBe(0);
|
||||
expect(engine.isFinished).toBe(true);
|
||||
});
|
||||
|
||||
it("ONE_TO_ONE: processo vai para Waiting apenas quando todas as threads estão em I/O", () => {
|
||||
const p = makeThreadedProcess({
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threads: [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 1 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t2",
|
||||
name: "T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 1 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const ticks = engine.runAll();
|
||||
|
||||
// Deve haver ticks em que o processo está Waiting (ambas as threads em I/O)
|
||||
const waitingTicks = ticks.filter(
|
||||
(tick) => tick.states["p1"] === "Waiting",
|
||||
);
|
||||
expect(waitingTicks.length).toBeGreaterThan(0);
|
||||
expect(engine.isFinished).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── MANY_TO_ONE ────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Engine — MANY_TO_ONE", () => {
|
||||
it("processo vai para Waiting quando qualquer thread está em I/O", () => {
|
||||
const p = makeThreadedProcess({
|
||||
threadModel: "MANY_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t2",
|
||||
name: "T2",
|
||||
// Esta thread ficaria pronta enquanto T1 está em I/O,
|
||||
// mas em MANY_TO_ONE o processo bloqueia inteiro
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const ticks = engine.runAll();
|
||||
|
||||
// Deve haver ticks em Waiting (T1 em I/O bloqueia o processo)
|
||||
const waitingTicks = ticks.filter(
|
||||
(tick) => tick.states["p1"] === "Waiting",
|
||||
);
|
||||
expect(waitingTicks.length).toBeGreaterThan(0);
|
||||
expect(engine.isFinished).toBe(true);
|
||||
});
|
||||
|
||||
it("processo termina quando todas as threads terminam", () => {
|
||||
const p = makeThreadedProcess({ threadModel: "MANY_TO_ONE" });
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
expect(engine.isFinished).toBe(true);
|
||||
expect(engine.runtimeStates[0]!.state).toBe("Terminated");
|
||||
const allTerminated = engine.threadRuntimeStates.every(
|
||||
(trt) => trt.state === "Terminated",
|
||||
);
|
||||
expect(allTerminated).toBe(true);
|
||||
});
|
||||
|
||||
it("MANY_TO_ONE tem mais ticks em Waiting que ONE_TO_ONE para mesmo workload", () => {
|
||||
const threads = [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu" as const, duration: 2 },
|
||||
{ type: "io" as const, duration: 4 },
|
||||
{ type: "cpu" as const, duration: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t2",
|
||||
name: "T2",
|
||||
bursts: [{ type: "cpu" as const, duration: 3 }],
|
||||
},
|
||||
];
|
||||
|
||||
const m2o = makeThreadedProcess({ threadModel: "MANY_TO_ONE", threads });
|
||||
const o2o = makeThreadedProcess({ threadModel: "ONE_TO_ONE", threads });
|
||||
|
||||
const engine1 = new SimulationEngine([m2o], BASE_CONFIG);
|
||||
const ticks1 = engine1.runAll();
|
||||
|
||||
const engine2 = new SimulationEngine([o2o], BASE_CONFIG);
|
||||
const ticks2 = engine2.runAll();
|
||||
|
||||
const waitingM2O = ticks1.filter(
|
||||
(t) => t.states["p1"] === "Waiting",
|
||||
).length;
|
||||
const waitingO2O = ticks2.filter(
|
||||
(t) => t.states["p1"] === "Waiting",
|
||||
).length;
|
||||
|
||||
// MANY_TO_ONE bloqueia o processo durante I/O da T1 mesmo com T2 pronta
|
||||
expect(waitingM2O).toBeGreaterThanOrEqual(waitingO2O);
|
||||
});
|
||||
});
|
||||
|
||||
// ── MANY_TO_MANY ───────────────────────────────────────────────────────────
|
||||
|
||||
describe("Engine — MANY_TO_MANY", () => {
|
||||
it("respeita limite de kernel threads (M=2 para 3 user threads)", () => {
|
||||
const p: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#CE93D8",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_MANY",
|
||||
kernelThreadCount: 2,
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 4 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
{ tid: "t3", name: "T3", bursts: [{ type: "cpu", duration: 2 }] },
|
||||
],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const ticks = engine.runAll();
|
||||
|
||||
expect(engine.isFinished).toBe(true);
|
||||
// Todos os threads devem ter terminado
|
||||
const allTerminated = engine.threadRuntimeStates.every(
|
||||
(trt) => trt.state === "Terminated",
|
||||
);
|
||||
expect(allTerminated).toBe(true);
|
||||
// Deve haver kernel wait info em algum tick
|
||||
const hasKernelWait = ticks.some(
|
||||
(tick) =>
|
||||
tick.kernelWaitingThreads !== undefined &&
|
||||
Object.keys(tick.kernelWaitingThreads).length > 0,
|
||||
);
|
||||
expect(hasKernelWait).toBe(true);
|
||||
});
|
||||
|
||||
it("termina corretamente com M=N (todos têm kernel thread)", () => {
|
||||
const p: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#CE93D8",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_MANY",
|
||||
kernelThreadCount: 3, // igual ao número de threads
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 2 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
{ tid: "t3", name: "T3", bursts: [{ type: "cpu", duration: 1 }] },
|
||||
],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
expect(engine.isFinished).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Snapshot de threads ────────────────────────────────────────────────────
|
||||
|
||||
describe("Engine — snapshot com threads", () => {
|
||||
it("threadStates presentes no snapshot quando processo tem threads", () => {
|
||||
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const tick = engine.step();
|
||||
expect(tick.threadStates).toBeDefined();
|
||||
expect(tick.threadStates!["p1"]).toBeDefined();
|
||||
expect(Object.keys(tick.threadStates!["p1"]!)).toContain("t1");
|
||||
});
|
||||
|
||||
it("cpuThreads indica thread ativa no processo", () => {
|
||||
const p = makeThreadedProcess({ threadModel: "ONE_TO_ONE" });
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
engine.step(); // tick 0 — despacha processo e thread
|
||||
const tick = engine.step(); // tick 1 — thread em execução
|
||||
expect(tick.cpuThreads).toBeDefined();
|
||||
expect(tick.cpuThreads!["p1"]).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Múltiplos processos com e sem threads ─────────────────────────────────
|
||||
|
||||
describe("Engine — mix de processos com e sem threads", () => {
|
||||
it("processa corretamente processos com e sem threads simultaneamente", () => {
|
||||
const pThreaded: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "Threaded",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threads: [
|
||||
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
],
|
||||
};
|
||||
|
||||
const pSimple: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "Simple",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([pThreaded, pSimple], BASE_CONFIG);
|
||||
engine.runAll();
|
||||
|
||||
expect(engine.isFinished).toBe(true);
|
||||
const rts = engine.runtimeStates;
|
||||
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Política intra-processo RR ─────────────────────────────────────────────
|
||||
|
||||
describe("Engine — RR intra-processo", () => {
|
||||
it("rotaciona threads dentro do processo com quantum 2", () => {
|
||||
const p = makeThreadedProcess({
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 2,
|
||||
threads: [
|
||||
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 6 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 4 }] },
|
||||
],
|
||||
});
|
||||
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const ticks = engine.runAll();
|
||||
|
||||
// Verifica que ambas as threads ficaram ativas em ticks diferentes
|
||||
const t1Active = ticks.filter(
|
||||
(tick) => tick.cpuThreads?.["p1"] === "t1",
|
||||
).length;
|
||||
const t2Active = ticks.filter(
|
||||
(tick) => tick.cpuThreads?.["p1"] === "t2",
|
||||
).length;
|
||||
|
||||
expect(t1Active).toBeGreaterThan(0);
|
||||
expect(t2Active).toBeGreaterThan(0);
|
||||
expect(engine.isFinished).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Quantum de processo com threads ────────────────────────────────────────
|
||||
//
|
||||
// Testa o comportamento correto quando o burst de uma thread termina
|
||||
// exatamente no mesmo tick em que o quantum do processo expira.
|
||||
// Antes do fix, o processo recebia quantum+1 ticks (bug).
|
||||
|
||||
describe("Engine — quantum de processo com threads (regressão)", () => {
|
||||
const rrConfig: SchedulerConfig = {
|
||||
algorithm: "RR",
|
||||
quantum: 2,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
};
|
||||
|
||||
const priorityRrConfig: SchedulerConfig = {
|
||||
algorithm: "PRIORITY_RR",
|
||||
quantum: 2,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
};
|
||||
|
||||
it("RR: segundo processo recebe CPU no tick 2 quando burst da thread = quantum", () => {
|
||||
// P1: thread T1 com CPU=2 (= quantum). Ao fim do tick 1, T1 termina E
|
||||
// o quantum expira ao mesmo tempo. P1 deve ceder a CPU imediatamente.
|
||||
// P2 deve começar no tick 2, não no tick 3.
|
||||
const p1: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "P1",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 2 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 2 }] },
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Tick 0 e 1: P1 roda (T1, burst=2=quantum)
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// Tick 2: P2 deve receber CPU (não tick 3)
|
||||
expect(cpu[2]).toBe("p2");
|
||||
|
||||
const rt2 = engine.runtimeStates.find((r) => r.processId === "p2")!;
|
||||
expect(rt2.startTick).toBe(2);
|
||||
});
|
||||
|
||||
it("RR: processo alterna exatamente a cada quantum quando bursts coincidem com quantum", () => {
|
||||
// Ambos os processos têm threads com CPU=2 = quantum.
|
||||
// Devem alternar estritamente: P1(0-1), P2(2-3), P1(4-5), P2(6-7)...
|
||||
const makeThreaded = (id: string, pid: number, color: string): Process => ({
|
||||
pid,
|
||||
id,
|
||||
name: id,
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color,
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE" as const,
|
||||
threadSchedulingPolicy: "FCFS" as const,
|
||||
threads: [
|
||||
{ tid: `${id}_t1`, name: "T1", bursts: [{ type: "cpu" as const, duration: 2 }] },
|
||||
{ tid: `${id}_t2`, name: "T2", bursts: [{ type: "cpu" as const, duration: 2 }] },
|
||||
],
|
||||
});
|
||||
|
||||
const engine = new SimulationEngine(
|
||||
[makeThreaded("p1", 1, "#4FC3F7"), makeThreaded("p2", 2, "#81C784")],
|
||||
rrConfig,
|
||||
);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Cada bloco de 2 ticks deve pertencer a um processo diferente
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
expect(cpu[2]).toBe("p2");
|
||||
expect(cpu[3]).toBe("p2");
|
||||
expect(cpu[4]).toBe("p1");
|
||||
expect(cpu[5]).toBe("p1");
|
||||
expect(cpu[6]).toBe("p2");
|
||||
expect(cpu[7]).toBe("p2");
|
||||
});
|
||||
|
||||
it("PRIORITY_RR: reproduz o cenário reportado — P2.startTick deve ser 2", () => {
|
||||
// Replica o config.json do usuário (versão simplificada):
|
||||
// App: ONE_TO_ONE, PRIORITY, High-1(prio=1, cpu=2), High-2(prio=2, cpu=2)
|
||||
// P2: ONE_TO_ONE, FCFS, T1(cpu=3)
|
||||
// Algoritmo: PRIORITY_RR, quantum=2, ambos prio=1
|
||||
// Bug: App rodava ticks 0,1,2 (quantum+1). Correto: 0,1 → P2 começa no tick 2.
|
||||
const app: Process = {
|
||||
pid: 1,
|
||||
id: "app",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 1,
|
||||
color: "#F06292",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{ tid: "h1", name: "High-1", priority: 1, bursts: [{ type: "cpu", duration: 2 }] },
|
||||
{ tid: "h2", name: "High-2", priority: 2, bursts: [{ type: "cpu", duration: 2 }] },
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 1,
|
||||
color: "#f472b6",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{ tid: "p2t1", name: "T1", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([app, p2], priorityRrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// App roda ticks 0 e 1 (quantum=2). P2 deve começar no tick 2.
|
||||
expect(cpu[0]).toBe("app");
|
||||
expect(cpu[1]).toBe("app");
|
||||
expect(cpu[2]).toBe("p2");
|
||||
|
||||
const p2Rt = engine.runtimeStates.find((r) => r.processId === "p2")!;
|
||||
expect(p2Rt.startTick).toBe(2);
|
||||
// responseTime de threads é calculado em _dispatchThreadForProcess
|
||||
const p2ThreadRts = engine.threadRuntimeStates.filter((t) => t.processId === "p2");
|
||||
expect(p2ThreadRts[0]!.startTick).toBe(2);
|
||||
expect(p2ThreadRts[0]!.responseTime).toBe(2);
|
||||
});
|
||||
|
||||
it("thread com burst menor que quantum não causa preempção prematura", () => {
|
||||
// T1 tem burst=1 (< quantum=2). T1 termina no tick 1 sem quantum expirado.
|
||||
// T2 deve continuar rodando no tick 2 (dentro do mesmo quantum).
|
||||
// P2 só recebe CPU no tick 3 (quando quantum do processo expirar com T2).
|
||||
const p1: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "P1",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{ tid: "t1", name: "T1", bursts: [{ type: "cpu", duration: 1 }] },
|
||||
{ tid: "t2", name: "T2", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 2 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Tick 0: P1/T1 roda (burst=1, quantum=2 → quantum não expirado)
|
||||
expect(cpu[0]).toBe("p1");
|
||||
// Tick 1: P1/T2 assume (quantum restante=1)
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// Tick 2: quantum do P1 expira → P2 recebe CPU
|
||||
expect(cpu[2]).toBe("p2");
|
||||
});
|
||||
|
||||
it("processo com thread em I/O no momento do quantum cede corretamente", () => {
|
||||
// T1: CPU(2) → IO(2) → CPU(1). Quantum=2.
|
||||
// T1 termina burst CPU no tick 1 E quantum=0: vai para IO, processo cede.
|
||||
// P2 deve receber CPU no tick 2.
|
||||
const p1: Process = {
|
||||
pid: 1,
|
||||
id: "p1",
|
||||
name: "P1",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const p2: Process = {
|
||||
pid: 2,
|
||||
id: "p2",
|
||||
name: "P2",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
};
|
||||
|
||||
const engine = new SimulationEngine([p1, p2], rrConfig);
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// Tick 2: T1 foi para IO e quantum expirou → P2 recebe CPU
|
||||
expect(cpu[2]).toBe("p2");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Política intra-processo PRIORITY ──────────────────────────────────────
|
||||
|
||||
describe("Engine — Priority intra-processo", () => {
|
||||
it("thread de maior prioridade roda primeiro (menor número = maior prio)", () => {
|
||||
const p = makeThreadedProcess({
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{
|
||||
tid: "t1",
|
||||
name: "Low",
|
||||
priority: 8,
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
{
|
||||
tid: "t2",
|
||||
name: "High",
|
||||
priority: 1,
|
||||
bursts: [{ type: "cpu", duration: 2 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const engine = new SimulationEngine([p], BASE_CONFIG);
|
||||
const firstTick = engine.step();
|
||||
|
||||
// t2 (prioridade 1) deve ser selecionada primeiro
|
||||
expect(firstTick.cpuThreads?.["p1"]).toBe("t2");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,396 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { MLFQAlgorithm } from "../algorithms/mlfq";
|
||||
import { SimulationEngine } from "../engine";
|
||||
import type { MlfqLevelDef, Process, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
|
||||
const mlfq = new MLFQAlgorithm();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
const DEFAULT_LEVELS: MlfqLevelDef[] = [
|
||||
{ quantum: 2 },
|
||||
{ quantum: 4 },
|
||||
{ quantum: 8 },
|
||||
];
|
||||
|
||||
function makeRt(
|
||||
id: string,
|
||||
opts: {
|
||||
arrivalTick?: number;
|
||||
quantumRemaining?: number;
|
||||
mlfqLevel?: number;
|
||||
waitingTime?: number;
|
||||
remainingBurst?: number;
|
||||
} = {},
|
||||
): ProcessRuntime {
|
||||
return {
|
||||
processId: id,
|
||||
state: "Ready",
|
||||
remainingBurst: opts.remainingBurst ?? 10,
|
||||
burstIndex: 0,
|
||||
arrivalTick: opts.arrivalTick ?? 0,
|
||||
startTick: null,
|
||||
finishTick: null,
|
||||
waitingTime: opts.waitingTime ?? 0,
|
||||
cpuTime: 0,
|
||||
ioTime: 0,
|
||||
responseTime: null,
|
||||
quantumRemaining: opts.quantumRemaining ?? 2,
|
||||
currentPriority: 5,
|
||||
mlfqLevel: opts.mlfqLevel,
|
||||
};
|
||||
}
|
||||
|
||||
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
|
||||
algorithm: "MLFQ",
|
||||
quantum: 2,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
mlfqLevels: DEFAULT_LEVELS,
|
||||
mlfqBoostInterval: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const p = (
|
||||
id: string,
|
||||
arrival: number,
|
||||
cpuDuration: number,
|
||||
): Process => ({
|
||||
id,
|
||||
name: id,
|
||||
arrivalTime: arrival,
|
||||
priority: 5,
|
||||
color: "#fff",
|
||||
bursts: [{ type: "cpu", duration: cpuDuration }],
|
||||
});
|
||||
|
||||
const pWithIo = (
|
||||
id: string,
|
||||
arrival: number,
|
||||
cpu1: number,
|
||||
io: number,
|
||||
cpu2: number,
|
||||
): Process => ({
|
||||
id,
|
||||
name: id,
|
||||
arrivalTime: arrival,
|
||||
priority: 5,
|
||||
color: "#fff",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: cpu1 },
|
||||
{ type: "io", duration: io },
|
||||
{ type: "cpu", duration: cpu2 },
|
||||
],
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: select() — initialization
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ select() — initialization", () => {
|
||||
it("returns null for empty queue", () => {
|
||||
expect(mlfq.select([], cfg())).toBeNull();
|
||||
});
|
||||
|
||||
it("assigns level 0 to a new process (mlfqLevel undefined)", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: undefined });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.mlfqLevel).toBe(0);
|
||||
});
|
||||
|
||||
it("does not re-initialize a process that already has mlfqLevel 0", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 2 });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.mlfqLevel).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: select() — demotion
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ select() — demotion", () => {
|
||||
it("demotes a quantum-expired process (quantumRemaining <= 0) from level 0 to 1", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 0 });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.mlfqLevel).toBe(1);
|
||||
});
|
||||
|
||||
it("demotes from level 1 to level 2", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 1, quantumRemaining: 0 });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.mlfqLevel).toBe(2);
|
||||
});
|
||||
|
||||
it("caps demotion at the last level (no demotion beyond level N-1)", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 2, quantumRemaining: 0 });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.mlfqLevel).toBe(2);
|
||||
});
|
||||
|
||||
it("sets quantumRemaining sentinel to 1 after demotion", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 0 });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.quantumRemaining).toBe(1);
|
||||
});
|
||||
|
||||
it("does NOT demote a process returning from I/O (quantumRemaining > 0)", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 0, quantumRemaining: 1 });
|
||||
mlfq.select([rt], cfg());
|
||||
expect(rt.mlfqLevel).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: select() — priority selection within/across levels
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ select() — level-based selection", () => {
|
||||
it("selects the process at the highest-priority level (lowest index)", () => {
|
||||
const low = makeRt("lo", { mlfqLevel: 2, quantumRemaining: 2 });
|
||||
const mid = makeRt("mid", { mlfqLevel: 1, quantumRemaining: 2 });
|
||||
const high = makeRt("hi", { mlfqLevel: 0, quantumRemaining: 2 });
|
||||
expect(mlfq.select([low, mid, high], cfg())?.processId).toBe("hi");
|
||||
expect(mlfq.select([mid, low], cfg())?.processId).toBe("mid");
|
||||
});
|
||||
|
||||
it("uses FIFO (arrivalTick) within the same level", () => {
|
||||
const a = makeRt("a", { mlfqLevel: 0, arrivalTick: 5, quantumRemaining: 2 });
|
||||
const b = makeRt("b", { mlfqLevel: 0, arrivalTick: 2, quantumRemaining: 2 });
|
||||
const c = makeRt("c", { mlfqLevel: 0, arrivalTick: 8, quantumRemaining: 2 });
|
||||
expect(mlfq.select([a, b, c], cfg())?.processId).toBe("b");
|
||||
});
|
||||
|
||||
it("uses processId as tiebreak when arrivalTick is equal", () => {
|
||||
const a = makeRt("z", { mlfqLevel: 1, arrivalTick: 3, quantumRemaining: 2 });
|
||||
const b = makeRt("a", { mlfqLevel: 1, arrivalTick: 3, quantumRemaining: 2 });
|
||||
expect(mlfq.select([a, b], cfg())?.processId).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: select() — priority boost
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ select() — priority boost", () => {
|
||||
it("does NOT boost when boostInterval is 0 (disabled)", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 100, quantumRemaining: 2 });
|
||||
mlfq.select([rt], cfg({ mlfqBoostInterval: 0 }));
|
||||
expect(rt.mlfqLevel).toBe(2);
|
||||
});
|
||||
|
||||
it("boosts a process to level 0 when waitingTime >= boostInterval", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 10, quantumRemaining: 2 });
|
||||
mlfq.select([rt], cfg({ mlfqBoostInterval: 10 }));
|
||||
expect(rt.mlfqLevel).toBe(0);
|
||||
});
|
||||
|
||||
it("does NOT boost a process that hasn't waited long enough", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 2, waitingTime: 9, quantumRemaining: 2 });
|
||||
mlfq.select([rt], cfg({ mlfqBoostInterval: 10 }));
|
||||
expect(rt.mlfqLevel).toBe(2);
|
||||
});
|
||||
|
||||
it("does NOT boost a process already at level 0", () => {
|
||||
const rt = makeRt("p1", { mlfqLevel: 0, waitingTime: 100, quantumRemaining: 2 });
|
||||
mlfq.select([rt], cfg({ mlfqBoostInterval: 5 }));
|
||||
expect(rt.mlfqLevel).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: shouldPreempt()
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ shouldPreempt()", () => {
|
||||
it("preempts when candidate is at a higher-priority level (lower index)", () => {
|
||||
const running = makeRt("r", { mlfqLevel: 2 });
|
||||
const candidate = makeRt("c", { mlfqLevel: 0 });
|
||||
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(true);
|
||||
});
|
||||
|
||||
it("preempts from level 1 by level 0", () => {
|
||||
const running = makeRt("r", { mlfqLevel: 1 });
|
||||
const candidate = makeRt("c", { mlfqLevel: 0 });
|
||||
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT preempt when candidate is at the same level", () => {
|
||||
const running = makeRt("r", { mlfqLevel: 1 });
|
||||
const candidate = makeRt("c", { mlfqLevel: 1 });
|
||||
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT preempt when candidate is at a lower-priority level", () => {
|
||||
const running = makeRt("r", { mlfqLevel: 0 });
|
||||
const candidate = makeRt("c", { mlfqLevel: 2 });
|
||||
expect(mlfq.shouldPreempt(running, candidate, cfg())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: isQuantumExpired()
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ isQuantumExpired()", () => {
|
||||
it("returns true when quantumRemaining <= 0", () => {
|
||||
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: 0 }), cfg())).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when quantumRemaining is negative", () => {
|
||||
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: -1 }), cfg())).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when quantumRemaining > 0", () => {
|
||||
expect(mlfq.isQuantumExpired(makeRt("p", { quantumRemaining: 1 }), cfg())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Unit: getQuantumForProcess()
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ getQuantumForProcess()", () => {
|
||||
it("returns the quantum for level 0 (default: 2)", () => {
|
||||
const rt = makeRt("p", { mlfqLevel: 0 });
|
||||
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(2);
|
||||
});
|
||||
|
||||
it("returns the quantum for level 1 (default: 4)", () => {
|
||||
const rt = makeRt("p", { mlfqLevel: 1 });
|
||||
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(4);
|
||||
});
|
||||
|
||||
it("returns the quantum for level 2 (default: 8)", () => {
|
||||
const rt = makeRt("p", { mlfqLevel: 2 });
|
||||
expect(mlfq.getQuantumForProcess(rt, cfg())).toBe(8);
|
||||
});
|
||||
|
||||
it("falls back to config.quantum when mlfqLevel is undefined", () => {
|
||||
const rt = makeRt("p", { mlfqLevel: undefined });
|
||||
expect(mlfq.getQuantumForProcess(rt, cfg({ quantum: 3 }))).toBe(2); // level 0 quantum
|
||||
});
|
||||
|
||||
it("respects custom level quantums", () => {
|
||||
const custom = cfg({ mlfqLevels: [{ quantum: 1 }, { quantum: 3 }, { quantum: 7 }] });
|
||||
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 0 }), custom)).toBe(1);
|
||||
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 1 }), custom)).toBe(3);
|
||||
expect(mlfq.getQuantumForProcess(makeRt("p", { mlfqLevel: 2 }), custom)).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Integration — CT-MLFQ-01: single process demotion sequence
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ integration — CT-MLFQ-01: demotion sequence", () => {
|
||||
it("single process is demoted through levels, with per-level quantum (CT-MLFQ-01)", () => {
|
||||
// P1: CPU burst of 14 ticks
|
||||
// Level 0: quantum=2 → runs ticks 0-1, demoted to level 1
|
||||
// Level 1: quantum=4 → runs ticks 2-5, demoted to level 2
|
||||
// Level 2: quantum=8 → runs ticks 6-13 (burst completes)
|
||||
const processes = [p("p1", 0, 14)];
|
||||
const engine = new SimulationEngine(processes, cfg());
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
expect(cpu).toHaveLength(14);
|
||||
expect(cpu.every((id) => id === "p1")).toBe(true);
|
||||
// Verify total cpuTime
|
||||
const rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
|
||||
expect(rt.cpuTime).toBe(14);
|
||||
expect(rt.state).toBe("Terminated");
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Integration — CT-MLFQ-02: new arrival preempts demoted process
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ integration — CT-MLFQ-02: preemption by new process", () => {
|
||||
it("a newly arrived process (level 0) preempts a level-1 running process (CT-MLFQ-02)", () => {
|
||||
// P1: arrives 0, CPU:10 — will be at level 1 after tick 2
|
||||
// P2: arrives 3, CPU:2 — enters at level 0, should preempt P1 at tick 3
|
||||
const processes = [p("p1", 0, 10), p("p2", 3, 2)];
|
||||
const engine = new SimulationEngine(processes, cfg());
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// P1 runs ticks 0-1 (quantum=2, level 0)
|
||||
expect(cpu[0]).toBe("p1");
|
||||
expect(cpu[1]).toBe("p1");
|
||||
// P1 is demoted to level 1 at tick 2, starts running with quantum=4
|
||||
expect(cpu[2]).toBe("p1");
|
||||
// P2 arrives at tick 3 (level 0), preempts P1 (level 1)
|
||||
expect(cpu[3]).toBe("p2");
|
||||
expect(cpu[4]).toBe("p2");
|
||||
// P2 done; P1 resumes at level 1
|
||||
expect(cpu[5]).toBe("p1");
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Integration — CT-MLFQ-03: I/O return does NOT demote
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ integration — CT-MLFQ-03: no demotion after I/O", () => {
|
||||
it("process returning from I/O stays at the same level (CT-MLFQ-03)", () => {
|
||||
// P1: CPU:1 → IO:2 → CPU:4
|
||||
// P1 uses only 1 tick at level 0 (quantum=2, returns from I/O with qR=1)
|
||||
// After I/O it should still be at level 0 and get a fresh quantum=2 on dispatch
|
||||
const processes = [pWithIo("p1", 0, 1, 2, 4)];
|
||||
const engine = new SimulationEngine(processes, cfg());
|
||||
const ticks = engine.runAll();
|
||||
const cpu = ticks.map((t) => t.cpuProcess);
|
||||
|
||||
// Tick 0: P1 runs (CPU burst 1 tick)
|
||||
expect(cpu[0]).toBe("p1");
|
||||
// Ticks 1-2: P1 in I/O (CPU=null)
|
||||
expect(cpu[1]).toBeNull();
|
||||
expect(cpu[2]).toBeNull();
|
||||
// Ticks 3-6: P1 runs remaining CPU burst (4 ticks)
|
||||
expect(cpu[3]).toBe("p1");
|
||||
expect(cpu[4]).toBe("p1");
|
||||
expect(cpu[5]).toBe("p1");
|
||||
expect(cpu[6]).toBe("p1");
|
||||
|
||||
const rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
|
||||
expect(rt.state).toBe("Terminated");
|
||||
expect(rt.cpuTime).toBe(5);
|
||||
expect(rt.ioTime).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Integration — CT-MLFQ-04: all processes terminate correctly
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQ integration — CT-MLFQ-04: correct termination", () => {
|
||||
it("all processes terminate with correct cpuTime", () => {
|
||||
const processes = [p("p1", 0, 3), p("p2", 0, 5), p("p3", 2, 4)];
|
||||
const engine = new SimulationEngine(processes, cfg());
|
||||
engine.runAll();
|
||||
const rts = engine.runtimeStates;
|
||||
|
||||
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
|
||||
expect(rts.find((r) => r.processId === "p1")!.cpuTime).toBe(3);
|
||||
expect(rts.find((r) => r.processId === "p2")!.cpuTime).toBe(5);
|
||||
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Metadata
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe("MLFQAlgorithm — metadata", () => {
|
||||
it("exposes correct educational properties", () => {
|
||||
expect(mlfq.name).toContain("MLFQ");
|
||||
expect(mlfq.isPreemptiveCapable).toBe(true);
|
||||
expect(mlfq.usesQuantum).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ThreadScheduler } from "../thread-scheduler";
|
||||
import type { ThreadRuntime } from "../types";
|
||||
|
||||
function makeThread(
|
||||
threadId: string,
|
||||
arrivalTick = 0,
|
||||
priority = 5,
|
||||
quantumRemaining = 2,
|
||||
): ThreadRuntime {
|
||||
return {
|
||||
threadId,
|
||||
processId: "p1",
|
||||
state: "Ready",
|
||||
remainingBurst: 4,
|
||||
burstIndex: 0,
|
||||
arrivalTick,
|
||||
startTick: null,
|
||||
finishTick: null,
|
||||
waitingTime: 0,
|
||||
cpuTime: 0,
|
||||
ioTime: 0,
|
||||
responseTime: null,
|
||||
quantumRemaining,
|
||||
currentPriority: priority,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ThreadScheduler — política FCFS", () => {
|
||||
const sched = new ThreadScheduler("FCFS", 2);
|
||||
|
||||
it("retorna null para fila vazia", () => {
|
||||
expect(sched.select([])).toBeNull();
|
||||
});
|
||||
|
||||
it("seleciona thread com menor arrivalTick", () => {
|
||||
const t1 = makeThread("t1", 5);
|
||||
const t2 = makeThread("t2", 2);
|
||||
const t3 = makeThread("t3", 8);
|
||||
expect(sched.select([t1, t2, t3])?.threadId).toBe("t2");
|
||||
});
|
||||
|
||||
it("desempate lexicográfico por threadId", () => {
|
||||
const ta = makeThread("ta", 0);
|
||||
const tb = makeThread("tb", 0);
|
||||
expect(sched.select([tb, ta])?.threadId).toBe("ta");
|
||||
});
|
||||
|
||||
it("isQuantumExpired sempre false em FCFS", () => {
|
||||
const t = makeThread("t1");
|
||||
t.quantumRemaining = -1;
|
||||
expect(sched.isQuantumExpired(t)).toBe(false);
|
||||
});
|
||||
|
||||
it("shouldPreemptThread sempre false em FCFS", () => {
|
||||
const curr = makeThread("curr", 0, 5);
|
||||
const cand = makeThread("cand", 0, 1);
|
||||
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ThreadScheduler — política RR", () => {
|
||||
const sched = new ThreadScheduler("RR", 3);
|
||||
|
||||
it("seleciona primeira da fila", () => {
|
||||
const t1 = makeThread("t1");
|
||||
const t2 = makeThread("t2");
|
||||
expect(sched.select([t1, t2])?.threadId).toBe("t1");
|
||||
});
|
||||
|
||||
it("isQuantumExpired true quando quantumRemaining ≤ 0", () => {
|
||||
const t = makeThread("t1");
|
||||
t.quantumRemaining = 0;
|
||||
expect(sched.isQuantumExpired(t)).toBe(true);
|
||||
t.quantumRemaining = -1;
|
||||
expect(sched.isQuantumExpired(t)).toBe(true);
|
||||
});
|
||||
|
||||
it("isQuantumExpired false quando quantum ainda disponível", () => {
|
||||
const t = makeThread("t1");
|
||||
t.quantumRemaining = 1;
|
||||
expect(sched.isQuantumExpired(t)).toBe(false);
|
||||
});
|
||||
|
||||
it("quantum correto", () => {
|
||||
expect(sched.quantum).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ThreadScheduler — política PRIORITY", () => {
|
||||
const sched = new ThreadScheduler("PRIORITY", 2);
|
||||
|
||||
it("seleciona thread com menor currentPriority (maior prioridade)", () => {
|
||||
const t1 = makeThread("t1", 0, 5);
|
||||
const t2 = makeThread("t2", 0, 1);
|
||||
const t3 = makeThread("t3", 0, 3);
|
||||
expect(sched.select([t1, t2, t3])?.threadId).toBe("t2");
|
||||
});
|
||||
|
||||
it("desempate por arrivalTick quando prioridades iguais", () => {
|
||||
const t1 = makeThread("t1", 3, 2);
|
||||
const t2 = makeThread("t2", 1, 2);
|
||||
expect(sched.select([t1, t2])?.threadId).toBe("t2");
|
||||
});
|
||||
|
||||
it("shouldPreemptThread true quando candidata tem prioridade maior", () => {
|
||||
const curr = makeThread("curr", 0, 5);
|
||||
const cand = makeThread("cand", 0, 2);
|
||||
expect(sched.shouldPreemptThread(curr, cand)).toBe(true);
|
||||
});
|
||||
|
||||
it("shouldPreemptThread false quando prioridade igual", () => {
|
||||
const curr = makeThread("curr", 0, 3);
|
||||
const cand = makeThread("cand", 0, 3);
|
||||
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
|
||||
});
|
||||
|
||||
it("shouldPreemptThread false quando candidata tem prioridade menor", () => {
|
||||
const curr = makeThread("curr", 0, 2);
|
||||
const cand = makeThread("cand", 0, 5);
|
||||
expect(sched.shouldPreemptThread(curr, cand)).toBe(false);
|
||||
});
|
||||
|
||||
it("isQuantumExpired sempre false em PRIORITY", () => {
|
||||
const t = makeThread("t1");
|
||||
t.quantumRemaining = -5;
|
||||
expect(sched.isQuantumExpired(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -66,4 +66,16 @@ export abstract class BaseAlgorithm {
|
||||
public static tiebreak(a: ProcessRuntime, b: ProcessRuntime): number {
|
||||
return _tiebreak(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o quantum a ser atribuído ao processo no momento do dispatch.
|
||||
* Algoritmos com quantum por processo (ex.: MLFQ) devem sobrescrever este método.
|
||||
* O padrão usa o quantum global da configuração.
|
||||
*/
|
||||
public getQuantumForProcess(
|
||||
_runtime: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): number {
|
||||
return config.quantum;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { PriorityNonPreemptiveAlgorithm, PriorityPreemptiveAlgorithm } from "./p
|
||||
import { PriorityRoundRobinAlgorithm } from "./priority_rr";
|
||||
import { HRRNAlgorithm } from "./hrrn";
|
||||
import { MultilevelQueueAlgorithm } from "./multilevel";
|
||||
import { MLFQAlgorithm } from "./mlfq";
|
||||
|
||||
export type { BaseAlgorithm };
|
||||
|
||||
@@ -28,4 +29,5 @@ export const algorithms: Record<SchedulingAlgorithm, BaseAlgorithm> = {
|
||||
PRIORITY_RR: new PriorityRoundRobinAlgorithm(),
|
||||
HRRN: new HRRNAlgorithm(),
|
||||
MULTILEVEL: new MultilevelQueueAlgorithm(),
|
||||
MLFQ: new MLFQAlgorithm(),
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { MlfqLevelDef, ProcessRuntime, SchedulerConfig } from "../types";
|
||||
import { BaseAlgorithm } from "./base";
|
||||
|
||||
/**
|
||||
* MLFQ — Multilevel Feedback Queue (Fila Multinível com Retroalimentação)
|
||||
*
|
||||
* Ao contrário do MLQ, os processos não são atribuídos permanentemente a filas.
|
||||
* Em vez disso, o comportamento de CPU de cada processo determina dinamicamente
|
||||
* em qual nível ele se encontra:
|
||||
*
|
||||
* - Todos os novos processos entram no nível 0 (maior prioridade).
|
||||
* - Quando um processo esgota seu quantum, é rebaixado ao nível seguinte.
|
||||
* - Quando um processo retorna de I/O, permanece no mesmo nível (sem rebaixamento).
|
||||
* - Processos em níveis mais altos preemptam processos em níveis mais baixos.
|
||||
* - Se `mlfqBoostInterval > 0`, processos que esperarem ≥ N ticks são
|
||||
* promovidos ao nível 0 para evitar inanição.
|
||||
*
|
||||
* Cada nível tem seu próprio quantum. O último nível não sofre mais rebaixamentos.
|
||||
* Dentro de um nível, a seleção é FIFO (por arrivalTick, depois processId).
|
||||
*/
|
||||
export class MLFQAlgorithm extends BaseAlgorithm {
|
||||
readonly name = "Multilevel Feedback Queue (MLFQ)";
|
||||
readonly description =
|
||||
"Filas de prioridade dinâmicas: novos processos entram no nível 0 e são " +
|
||||
"rebaixados ao usar todo o quantum. Retorno de I/O não causa rebaixamento. " +
|
||||
"Boost opcional promove processos que esperaram muito tempo de volta ao nível 0.";
|
||||
readonly isPreemptiveCapable = true;
|
||||
readonly usesQuantum = true;
|
||||
|
||||
static readonly DEFAULT_LEVELS: MlfqLevelDef[] = [
|
||||
{ quantum: 2 },
|
||||
{ quantum: 4 },
|
||||
{ quantum: 8 },
|
||||
];
|
||||
|
||||
select(
|
||||
readyQueue: readonly ProcessRuntime[],
|
||||
config: SchedulerConfig,
|
||||
): ProcessRuntime | null {
|
||||
if (readyQueue.length === 0) return null;
|
||||
|
||||
const levels = config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS;
|
||||
const maxLevel = levels.length - 1;
|
||||
const boostInterval = config.mlfqBoostInterval ?? 0;
|
||||
|
||||
// Phase 1 — initialize new processes and apply demotion.
|
||||
// A process with quantumRemaining <= 0 and an already-initialized mlfqLevel
|
||||
// was preempted by quantum expiry → demote it.
|
||||
for (const rt of readyQueue) {
|
||||
if (rt.mlfqLevel === undefined) {
|
||||
// New process: assign level 0.
|
||||
rt.mlfqLevel = 0;
|
||||
} else if (rt.quantumRemaining <= 0) {
|
||||
// Quantum-expired: demote (capped at last level).
|
||||
rt.mlfqLevel = Math.min(rt.mlfqLevel + 1, maxLevel);
|
||||
// Set sentinel so we don't demote again before the next dispatch.
|
||||
rt.quantumRemaining = 1;
|
||||
}
|
||||
// quantumRemaining > 0 → returned from I/O or priority-preempted; no demotion.
|
||||
}
|
||||
|
||||
// Phase 2 — priority boost.
|
||||
if (boostInterval > 0) {
|
||||
for (const rt of readyQueue) {
|
||||
if ((rt.mlfqLevel ?? 0) > 0 && rt.waitingTime >= boostInterval) {
|
||||
rt.mlfqLevel = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3 — pick the process at the highest level (lowest index), FIFO within.
|
||||
let best: ProcessRuntime | null = null;
|
||||
for (const rt of readyQueue) {
|
||||
if (best === null) {
|
||||
best = rt;
|
||||
continue;
|
||||
}
|
||||
const rtLevel = rt.mlfqLevel ?? 0;
|
||||
const bestLevel = best.mlfqLevel ?? 0;
|
||||
if (
|
||||
rtLevel < bestLevel ||
|
||||
(rtLevel === bestLevel && BaseAlgorithm.tiebreak(rt, best) < 0)
|
||||
) {
|
||||
best = rt;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
shouldPreempt(
|
||||
current: ProcessRuntime,
|
||||
candidate: ProcessRuntime,
|
||||
_config: SchedulerConfig,
|
||||
): boolean {
|
||||
// A candidate at a higher-priority level (lower index) preempts the running process.
|
||||
return (candidate.mlfqLevel ?? 0) < (current.mlfqLevel ?? 0);
|
||||
}
|
||||
|
||||
isQuantumExpired(
|
||||
runtime: ProcessRuntime,
|
||||
_config: SchedulerConfig,
|
||||
): boolean {
|
||||
return runtime.quantumRemaining <= 0;
|
||||
}
|
||||
|
||||
override getQuantumForProcess(
|
||||
runtime: ProcessRuntime,
|
||||
config: SchedulerConfig,
|
||||
): number {
|
||||
const levels = config.mlfqLevels ?? MLFQAlgorithm.DEFAULT_LEVELS;
|
||||
const level = runtime.mlfqLevel ?? 0;
|
||||
return levels[level]?.quantum ?? config.quantum;
|
||||
}
|
||||
}
|
||||
+677
-129
@@ -4,9 +4,12 @@ import type {
|
||||
SchedulerConfig,
|
||||
SimTick,
|
||||
StateLabel,
|
||||
Thread,
|
||||
ThreadRuntime,
|
||||
} from "./types";
|
||||
import { algorithms } from "./algorithms/index";
|
||||
import type { BaseAlgorithm } from "./algorithms/index";
|
||||
import { ThreadScheduler } from "./thread-scheduler";
|
||||
|
||||
/**
|
||||
* SimulationEngine — Núcleo de execução da simulação de escalonamento.
|
||||
@@ -14,42 +17,66 @@ import type { BaseAlgorithm } from "./algorithms/index";
|
||||
* Responsável por avançar o simulador tick a tick, aplicando o algoritmo
|
||||
* de escalonamento configurado e mantendo o estado de todos os processos.
|
||||
*
|
||||
* Suporta dois modos de operação:
|
||||
* - Processos simples: comportamento original, sem alterações.
|
||||
* - Processos com threads: escalonamento em dois níveis:
|
||||
* 1. Nível de processo — algoritmo global do SchedulerConfig.
|
||||
* 2. Nível de thread — ThreadScheduler com política intra-processo.
|
||||
*
|
||||
* Modelos de threads suportados:
|
||||
* MANY_TO_ONE — Uma kernel thread por processo. I/O bloqueia todo o processo.
|
||||
* ONE_TO_ONE — Uma kernel thread por thread de usuário. Threads bloqueiam
|
||||
* independentemente; o processo só bloqueia se todas as threads
|
||||
* estiverem em I/O.
|
||||
* MANY_TO_MANY — M kernel threads para N threads de usuário (M ≤ N).
|
||||
* Slots são liberados durante I/O e reatribuídos dinamicamente.
|
||||
*
|
||||
* Ciclo de vida de um único tick (método `step()`):
|
||||
* 1. Chegada — processos cujo `arrivalTime` é igual ao tick atual
|
||||
* transitam New → Ready e entram na fila de prontos.
|
||||
* 2. Preempção — o algoritmo decide se o processo em CPU deve ceder
|
||||
* lugar a um candidato mais prioritário.
|
||||
* 3. Despacho — seleciona e despacha o próximo processo (com overhead
|
||||
* de troca de contexto, se configurado).
|
||||
* 4. Snapshot — captura o estado de todos os processos neste tick.
|
||||
* 5. Passagem do tempo — decrementa bursts, acumula tempos de CPU/IO/espera.
|
||||
* 6. Aging — decrementa prioridade dos processos em espera prolongada.
|
||||
* 7. Conclusões — trata término de bursts de CPU e IO; avança para o
|
||||
* próximo burst ou termina o processo.
|
||||
*
|
||||
* Uso típico:
|
||||
* ```ts
|
||||
* const engine = new SimulationEngine(processes, config);
|
||||
* const ticks = engine.runAll(); // executa até todos os processos terminarem
|
||||
* ```
|
||||
*
|
||||
* Invariante: após `runAll()`, `isFinished` é `true` e todos os runtimes
|
||||
* têm `state === "Terminated"`.
|
||||
* 1. Chegada — New → Ready para processos/threads que chegam neste tick.
|
||||
* 2. Preempção — Algoritmo decide se processo em CPU deve ceder lugar.
|
||||
* 3. Despacho — Seleciona e despacha próximo processo (e thread).
|
||||
* 4. Snapshot — Captura estado completo (processos + threads).
|
||||
* 5. Passagem do tempo — Decrementa bursts, acumula tempos.
|
||||
* 6. Aging — Envelhecimento de prioridade anti-starvation.
|
||||
* 7. Conclusões — Trata término de bursts de CPU/IO (processo e threads).
|
||||
*/
|
||||
export class SimulationEngine {
|
||||
// ─── Campos privados ──────────────────────────────────────────────────────
|
||||
// ─── Campos privados — processos ──────────────────────────────────────────
|
||||
|
||||
private readonly _processes: Process[];
|
||||
private readonly _config: SchedulerConfig;
|
||||
private readonly _algorithm: BaseAlgorithm;
|
||||
|
||||
private _tick: number; // começa em -1; incrementado no início de step()
|
||||
private _tick: number;
|
||||
private _runtimes: Map<string, ProcessRuntime>;
|
||||
private _cpuProcess: ProcessRuntime | null;
|
||||
private _readyQueue: ProcessRuntime[]; // array ordenado (FIFO para RR)
|
||||
private _readyQueue: ProcessRuntime[];
|
||||
private _ioQueue: ProcessRuntime[];
|
||||
private _contextSwitchRemaining: number;
|
||||
private _prevCpuProcessId: string | null; // para detectar troca de contexto
|
||||
private _prevCpuProcessId: string | null;
|
||||
|
||||
// ─── Campos privados — threads ────────────────────────────────────────────
|
||||
|
||||
/** threadRuntimes[processId][threadId] = ThreadRuntime */
|
||||
private _threadRuntimes: Map<string, Map<string, ThreadRuntime>>;
|
||||
|
||||
/** Escalonador intra-processo por processo */
|
||||
private _threadSchedulers: Map<string, ThreadScheduler>;
|
||||
|
||||
/** Thread atualmente ativa na CPU para cada processo com threads */
|
||||
private _activeThreads: Map<string, ThreadRuntime | null>;
|
||||
|
||||
/** Threads em I/O por processo */
|
||||
private _threadIoQueues: Map<string, ThreadRuntime[]>;
|
||||
|
||||
/** Fila de threads prontas intra-processo */
|
||||
private _readyThreadQueues: Map<string, ThreadRuntime[]>;
|
||||
|
||||
/** MANY_TO_MANY: slots de kernel disponíveis por processo */
|
||||
private _kernelSlotsAvailable: Map<string, number>;
|
||||
|
||||
/** MANY_TO_MANY: threads aguardando slot de kernel */
|
||||
private _kernelWaitQueues: Map<string, ThreadRuntime[]>;
|
||||
|
||||
// ─── Construtor ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -64,37 +91,47 @@ export class SimulationEngine {
|
||||
this._ioQueue = [];
|
||||
this._contextSwitchRemaining = 0;
|
||||
this._prevCpuProcessId = null;
|
||||
|
||||
// Inicializa runtimes de processo
|
||||
this._runtimes = new Map(
|
||||
processes.map((p) => [p.id, SimulationEngine._initRuntime(p, config)]),
|
||||
processes.map((p) => [p.id, SimulationEngine._initProcessRuntime(p, config)]),
|
||||
);
|
||||
|
||||
// Inicializa estruturas de threads
|
||||
this._threadRuntimes = new Map();
|
||||
this._threadSchedulers = new Map();
|
||||
this._activeThreads = new Map();
|
||||
this._threadIoQueues = new Map();
|
||||
this._readyThreadQueues = new Map();
|
||||
this._kernelSlotsAvailable = new Map();
|
||||
this._kernelWaitQueues = new Map();
|
||||
|
||||
for (const p of processes) {
|
||||
if (this._hasThreads(p.id)) {
|
||||
this._initThreadStructures(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── API pública ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Avança a simulação por um tick e retorna o snapshot do estado neste tick.
|
||||
*
|
||||
* Lança um erro se a simulação já tiver terminado (`isFinished === true`).
|
||||
* O número do tick é incrementado no início; o primeiro tick válido é 0.
|
||||
*/
|
||||
step(): SimTick {
|
||||
if (this.isFinished) throw new Error("Simulation is already finished");
|
||||
|
||||
this._tick++;
|
||||
|
||||
this._handleArrivals(); // 1. New → Ready para quem chega agora
|
||||
this._checkPreemption(); // 2. Preempta CPU se candidato melhor chegou
|
||||
this._dispatchNext(); // 3. Despacha próximo (ou inicia context switch)
|
||||
const snapshot = this._snapshot(); // 4. Captura estado DURANTE este tick
|
||||
this._advanceTime(); // 5. Decrementa bursts, acumula métricas
|
||||
this._applyAging(); // 6. Aging de prioridade (se habilitado)
|
||||
this._handleCpuCompletion(); // 7a. Finaliza burst/quantum de CPU
|
||||
this._handleIoCompletions(); // 7b. Finaliza bursts de I/O
|
||||
this._handleArrivals();
|
||||
this._checkPreemption();
|
||||
this._dispatchNext();
|
||||
const snapshot = this._snapshot();
|
||||
this._advanceTime();
|
||||
this._applyAging();
|
||||
this._handleCpuCompletion();
|
||||
this._handleIoCompletions();
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Executa até o fim e retorna todos os snapshots de tick. */
|
||||
runAll(): SimTick[] {
|
||||
const results: SimTick[] = [];
|
||||
while (!this.isFinished) {
|
||||
@@ -106,7 +143,6 @@ export class SimulationEngine {
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Verdadeiro quando todos os processos estão no estado "Terminated". */
|
||||
get isFinished(): boolean {
|
||||
for (const rt of this._runtimes.values()) {
|
||||
if (rt.state !== "Terminated") return false;
|
||||
@@ -114,23 +150,34 @@ export class SimulationEngine {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Número do tick atual (−1 antes do primeiro `step()`). */
|
||||
get currentTick(): number {
|
||||
return this._tick;
|
||||
}
|
||||
|
||||
/** Array com o estado de runtime de todos os processos. */
|
||||
get runtimeStates(): ProcessRuntime[] {
|
||||
return Array.from(this._runtimes.values());
|
||||
}
|
||||
|
||||
/** Retorna todos os runtimes de threads de todos os processos. */
|
||||
get threadRuntimeStates(): ThreadRuntime[] {
|
||||
const result: ThreadRuntime[] = [];
|
||||
for (const threadMap of this._threadRuntimes.values()) {
|
||||
for (const trt of threadMap.values()) {
|
||||
result.push(trt);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Informa se um processo possui threads definidas. */
|
||||
processHasThreads(processId: string): boolean {
|
||||
return this._hasThreads(processId);
|
||||
}
|
||||
|
||||
// ─── Fases do ciclo de simulação ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fase 1 — Chegada de processos.
|
||||
*
|
||||
* Processos cujo `arrivalTime` é igual ao tick atual transitam do estado
|
||||
* "New" para "Ready" e são inseridos na fila de prontos.
|
||||
* Fase 1 — Chegada de processos (e inicialização de threads).
|
||||
*/
|
||||
private _handleArrivals(): void {
|
||||
for (const p of this._processes) {
|
||||
@@ -138,17 +185,40 @@ export class SimulationEngine {
|
||||
if (p.arrivalTime === this._tick && rt.state === "New") {
|
||||
this._transition(rt, "Ready");
|
||||
this._readyQueue.push(rt);
|
||||
|
||||
if (this._hasThreads(p.id)) {
|
||||
// Transiciona todas as threads New → Ready na fila intra-processo
|
||||
const threadMap = this._threadRuntimes.get(p.id)!;
|
||||
const readyQueue = this._readyThreadQueues.get(p.id)!;
|
||||
const model = p.threadModel ?? "ONE_TO_ONE";
|
||||
|
||||
for (const trt of threadMap.values()) {
|
||||
if (trt.state === "New") {
|
||||
if (model === "MANY_TO_MANY") {
|
||||
const slotsAvail = this._kernelSlotsAvailable.get(p.id) ?? 0;
|
||||
if (slotsAvail > 0) {
|
||||
this._kernelSlotsAvailable.set(p.id, slotsAvail - 1);
|
||||
this._transitionThread(trt, "Ready");
|
||||
readyQueue.push(trt);
|
||||
} else {
|
||||
// Sem slot disponível → espera kernel thread
|
||||
trt.kernelWaiting = true;
|
||||
this._transitionThread(trt, "Ready");
|
||||
this._kernelWaitQueues.get(p.id)!.push(trt);
|
||||
}
|
||||
} else {
|
||||
this._transitionThread(trt, "Ready");
|
||||
readyQueue.push(trt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fase 2 — Verificação de preempção.
|
||||
*
|
||||
* Consulta o algoritmo: se houver um candidato na fila de prontos que deva
|
||||
* preemptar o processo em CPU, o processo atual retorna à fila de prontos
|
||||
* e `_cpuProcess` é zerado. Chamado após a chegada para que processos
|
||||
* recém-chegados possam imediatamente preemptar.
|
||||
* Fase 2 — Verificação de preempção no nível de processo.
|
||||
*/
|
||||
private _checkPreemption(): void {
|
||||
if (this._cpuProcess === null || this._readyQueue.length === 0) return;
|
||||
@@ -158,6 +228,17 @@ export class SimulationEngine {
|
||||
this._algorithm.shouldPreempt(this._cpuProcess, candidate, this._config)
|
||||
) {
|
||||
const cpu = this._cpuProcess;
|
||||
|
||||
// Retorna thread ativa à fila intra-processo (se processo com threads)
|
||||
if (this._hasThreads(cpu.processId)) {
|
||||
const activeThread = this._activeThreads.get(cpu.processId) ?? null;
|
||||
if (activeThread !== null) {
|
||||
this._transitionThread(activeThread, "Ready");
|
||||
this._readyThreadQueues.get(cpu.processId)!.unshift(activeThread);
|
||||
this._activeThreads.set(cpu.processId, null);
|
||||
}
|
||||
}
|
||||
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
@@ -165,16 +246,7 @@ export class SimulationEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fase 3 — Despacho / troca de contexto.
|
||||
*
|
||||
* Se um context switch estiver em andamento, nenhum processo é despachado
|
||||
* neste tick. Caso contrário, e se a CPU estiver ociosa com processos
|
||||
* prontos, o algoritmo seleciona o próximo processo:
|
||||
*
|
||||
* - Se houver overhead de troca de contexto configurado e o processo
|
||||
* selecionado for diferente do último, o processo volta ao início da
|
||||
* fila e o contador de context-switch é iniciado.
|
||||
* - Caso contrário, o processo é despachado imediatamente para a CPU.
|
||||
* Fase 3 — Despacho de processo (e thread).
|
||||
*/
|
||||
private _dispatchNext(): void {
|
||||
if (this._contextSwitchRemaining > 0) return;
|
||||
@@ -196,21 +268,71 @@ export class SimulationEngine {
|
||||
this._prevCpuProcessId = next.processId;
|
||||
} else {
|
||||
if (next.startTick === null) next.startTick = this._tick;
|
||||
next.quantumRemaining = this._config.quantum;
|
||||
next.quantumRemaining = this._algorithm.getQuantumForProcess(next, this._config);
|
||||
this._transition(next, "Running");
|
||||
this._prevCpuProcessId = next.processId;
|
||||
this._cpuProcess = next;
|
||||
|
||||
// Despacha thread intra-processo (se aplicável)
|
||||
if (this._hasThreads(next.processId)) {
|
||||
this._dispatchThreadForProcess(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fase 4 — Snapshot do estado.
|
||||
*/
|
||||
private _snapshot(): SimTick {
|
||||
const states: Record<string, StateLabel> = {};
|
||||
for (const [id, rt] of this._runtimes) {
|
||||
states[id] = rt.state;
|
||||
}
|
||||
const contextSwitching = this._contextSwitchRemaining > 0;
|
||||
|
||||
const snap: SimTick = {
|
||||
tick: this._tick,
|
||||
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
|
||||
ioProcesses: this._ioQueue.map((rt) => rt.processId),
|
||||
readyQueue: this._readyQueue.map((rt) => rt.processId),
|
||||
states,
|
||||
contextSwitching,
|
||||
ctxSwitchForProcess: contextSwitching
|
||||
? (this._readyQueue[0]?.processId ?? null)
|
||||
: null,
|
||||
};
|
||||
|
||||
// Inclui estado de threads se houver processos com threads
|
||||
if (this._threadRuntimes.size > 0) {
|
||||
const threadStates: Record<string, Record<string, StateLabel>> = {};
|
||||
const cpuThreads: Record<string, string | null> = {};
|
||||
const kernelWaitingThreads: Record<string, string[]> = {};
|
||||
|
||||
for (const [processId, threadMap] of this._threadRuntimes) {
|
||||
threadStates[processId] = {};
|
||||
for (const [threadId, trt] of threadMap) {
|
||||
threadStates[processId][threadId] = trt.state;
|
||||
}
|
||||
const active = this._activeThreads.get(processId) ?? null;
|
||||
cpuThreads[processId] = active?.threadId ?? null;
|
||||
const kwQueue = this._kernelWaitQueues.get(processId) ?? [];
|
||||
if (kwQueue.length > 0) {
|
||||
kernelWaitingThreads[processId] = kwQueue.map((t) => t.threadId);
|
||||
}
|
||||
}
|
||||
|
||||
snap.threadStates = threadStates;
|
||||
snap.cpuThreads = cpuThreads;
|
||||
if (Object.keys(kernelWaitingThreads).length > 0) {
|
||||
snap.kernelWaitingThreads = kernelWaitingThreads;
|
||||
}
|
||||
}
|
||||
|
||||
return snap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fase 5 — Passagem do tempo.
|
||||
*
|
||||
* Simula a execução de 1 tick:
|
||||
* - Decrementa o contador de context-switch restante (se ativo).
|
||||
* - Decrementa o burst restante do processo em CPU e acumula cpuTime/quantum.
|
||||
* - Decrementa o burst restante de cada processo em I/O e acumula ioTime.
|
||||
* - Incrementa o waitingTime de todos os processos na fila de prontos.
|
||||
*/
|
||||
private _advanceTime(): void {
|
||||
if (this._contextSwitchRemaining > 0) {
|
||||
@@ -219,29 +341,67 @@ export class SimulationEngine {
|
||||
|
||||
if (this._cpuProcess !== null) {
|
||||
const cpu = this._cpuProcess;
|
||||
|
||||
if (this._hasThreads(cpu.processId)) {
|
||||
// CPU time acumulado no nível de processo
|
||||
cpu.cpuTime++;
|
||||
cpu.quantumRemaining--;
|
||||
|
||||
// Decrementa burst e tempo da thread ativa
|
||||
const activeThread = this._activeThreads.get(cpu.processId) ?? null;
|
||||
if (activeThread !== null) {
|
||||
activeThread.remainingBurst--;
|
||||
activeThread.cpuTime++;
|
||||
activeThread.quantumRemaining--;
|
||||
// Sincroniza processo com thread ativa
|
||||
cpu.remainingBurst = activeThread.remainingBurst;
|
||||
}
|
||||
} else {
|
||||
cpu.remainingBurst--;
|
||||
cpu.cpuTime++;
|
||||
cpu.quantumRemaining--;
|
||||
}
|
||||
}
|
||||
|
||||
// I/O de processos sem threads
|
||||
for (const rt of this._ioQueue) {
|
||||
if (!this._hasThreads(rt.processId)) {
|
||||
rt.remainingBurst--;
|
||||
rt.ioTime++;
|
||||
}
|
||||
// Processos com threads (MANY_TO_ONE): tempo de I/O acumulado abaixo
|
||||
}
|
||||
|
||||
// I/O de threads (todos os modelos)
|
||||
for (const [, threadIoQueue] of this._threadIoQueues) {
|
||||
for (const trt of threadIoQueue) {
|
||||
trt.remainingBurst--;
|
||||
trt.ioTime++;
|
||||
}
|
||||
}
|
||||
|
||||
// Tempo de espera — fila de prontos de processos
|
||||
for (const rt of this._readyQueue) {
|
||||
rt.waitingTime++;
|
||||
}
|
||||
|
||||
// Tempo de espera — fila de prontas de threads intra-processo
|
||||
for (const [, threadReadyQueue] of this._readyThreadQueues) {
|
||||
for (const trt of threadReadyQueue) {
|
||||
trt.waitingTime++;
|
||||
}
|
||||
}
|
||||
|
||||
// Tempo de espera — threads em kernel wait (MANY_TO_MANY)
|
||||
for (const [, kwQueue] of this._kernelWaitQueues) {
|
||||
for (const trt of kwQueue) {
|
||||
trt.waitingTime++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fase 6 — Aging (envelhecimento de prioridade).
|
||||
*
|
||||
* Quando o aging está habilitado, a cada `agingInterval` ticks o valor de
|
||||
* `currentPriority` de todos os processos na fila de prontos é decrementado
|
||||
* em 1 (até o mínimo de 1). Isso previne inanição em algoritmos de prioridade.
|
||||
*
|
||||
* Não é executado no tick 0 para evitar decremento prematuro.
|
||||
* Fase 6 — Aging de prioridade.
|
||||
*/
|
||||
private _applyAging(): void {
|
||||
if (
|
||||
@@ -258,24 +418,20 @@ export class SimulationEngine {
|
||||
|
||||
/**
|
||||
* Fase 7a — Conclusão de burst de CPU.
|
||||
*
|
||||
* Após a passagem do tempo, verifica se o processo em CPU terminou seu burst
|
||||
* ou esgotou o quantum:
|
||||
*
|
||||
* - Burst zerado: avança para o próximo burst do processo.
|
||||
* · Se o próximo for I/O → transita para "Waiting" e entra na ioQueue.
|
||||
* · Se o próximo for CPU → retorna à fila de prontos ("Ready").
|
||||
* · Se não houver próximo → termina o processo ("Terminated").
|
||||
*
|
||||
* - Quantum expirado (sem término de burst): retorna à fila de prontos.
|
||||
*/
|
||||
private _handleCpuCompletion(): void {
|
||||
if (this._cpuProcess === null) return;
|
||||
|
||||
const cpu = this._cpuProcess;
|
||||
|
||||
if (this._hasThreads(cpu.processId)) {
|
||||
this._handleThreadedCpuCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Processo sem threads (comportamento original) ──────────────────────
|
||||
if (cpu.remainingBurst === 0) {
|
||||
this._advanceBurst(cpu);
|
||||
cpu.quantumRemaining = 1;
|
||||
const process = this._processes.find((p) => p.id === cpu.processId)!;
|
||||
const nextBurst = process.bursts[cpu.burstIndex];
|
||||
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||
@@ -298,16 +454,16 @@ export class SimulationEngine {
|
||||
|
||||
/**
|
||||
* Fase 7b — Conclusão de bursts de I/O.
|
||||
*
|
||||
* Percorre a ioQueue e, para cada processo cujo burst de I/O zerou:
|
||||
* - Se houver próximo burst → transita para "Ready" e insere na readyQueue.
|
||||
* - Caso contrário → termina o processo ("Terminated").
|
||||
*
|
||||
* Processos ainda com I/O pendente permanecem na ioQueue.
|
||||
*/
|
||||
private _handleIoCompletions(): void {
|
||||
// ── Processos sem threads ──────────────────────────────────────────────
|
||||
const nextIoQueue: ProcessRuntime[] = [];
|
||||
for (const rt of this._ioQueue) {
|
||||
if (this._hasThreads(rt.processId)) {
|
||||
// Processos com threads gerenciados em _handleThreadIoCompletions
|
||||
nextIoQueue.push(rt);
|
||||
continue;
|
||||
}
|
||||
if (rt.remainingBurst === 0) {
|
||||
this._advanceBurst(rt);
|
||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||
@@ -323,22 +479,359 @@ export class SimulationEngine {
|
||||
}
|
||||
}
|
||||
this._ioQueue = nextIoQueue;
|
||||
|
||||
// ── Threads em I/O ────────────────────────────────────────────────────
|
||||
this._handleThreadIoCompletions();
|
||||
}
|
||||
|
||||
// ─── Lógica de threads ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Despacha a próxima thread intra-processo quando o processo recebe CPU.
|
||||
*/
|
||||
private _dispatchThreadForProcess(processRt: ProcessRuntime): void {
|
||||
const processId = processRt.processId;
|
||||
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
|
||||
const scheduler = this._threadSchedulers.get(processId)!;
|
||||
|
||||
const nextThread = scheduler.select(readyThreadQueue);
|
||||
if (nextThread === null) {
|
||||
// Nenhuma thread pronta — mantém processo em Running mas sem thread ativa
|
||||
// (Pode ocorrer se todas as threads estão em I/O; ONE_TO_ONE pode despachar
|
||||
// o processo mesmo assim para que ele "exista" na CPU enquanto threads esperam)
|
||||
this._activeThreads.set(processId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = readyThreadQueue.indexOf(nextThread);
|
||||
if (idx !== -1) readyThreadQueue.splice(idx, 1);
|
||||
|
||||
if (nextThread.startTick === null) {
|
||||
nextThread.startTick = this._tick;
|
||||
nextThread.responseTime = this._tick - nextThread.arrivalTick;
|
||||
}
|
||||
nextThread.quantumRemaining = scheduler.quantum;
|
||||
nextThread.kernelWaiting = false;
|
||||
this._transitionThread(nextThread, "Running");
|
||||
this._activeThreads.set(processId, nextThread);
|
||||
|
||||
// Sincroniza process runtime com thread ativa
|
||||
processRt.remainingBurst = nextThread.remainingBurst;
|
||||
if (processRt.startTick === null) {
|
||||
processRt.startTick = this._tick;
|
||||
processRt.responseTime = this._tick - processRt.arrivalTick;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerencia conclusão de burst de CPU para processos com threads.
|
||||
*/
|
||||
private _handleThreadedCpuCompletion(): void {
|
||||
const cpu = this._cpuProcess!;
|
||||
const processId = cpu.processId;
|
||||
const process = this._processes.find((p) => p.id === processId)!;
|
||||
const policy = process.threadSchedulingPolicy ?? "FCFS";
|
||||
const scheduler = this._threadSchedulers.get(processId)!;
|
||||
const activeThread = this._activeThreads.get(processId) ?? null;
|
||||
|
||||
if (activeThread === null) {
|
||||
// Sem thread ativa — verifica se processo deve continuar ou aguardar
|
||||
if (this._readyThreadQueues.get(processId)!.length > 0) {
|
||||
this._dispatchThreadForProcess(cpu);
|
||||
} else {
|
||||
// Todas as threads em I/O: processo vai para Waiting
|
||||
this._transition(cpu, "Waiting");
|
||||
if (!this._ioQueue.includes(cpu)) this._ioQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeThread.remainingBurst === 0) {
|
||||
// ── CPU burst da thread concluído ────────────────────────────────────
|
||||
this._activeThreads.set(processId, null);
|
||||
this._handleThreadBurstComplete(cpu, activeThread, process);
|
||||
|
||||
// Se o quantum do processo também expirou neste mesmo tick, cede a CPU
|
||||
// agora (após tratar o burst) em vez de dar um tick extra ao processo.
|
||||
if (this._cpuProcess !== null && this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||
const activeAfter = this._activeThreads.get(processId) ?? null;
|
||||
if (activeAfter !== null) {
|
||||
this._transitionThread(activeAfter, "Ready");
|
||||
if (policy === "RR") {
|
||||
this._readyThreadQueues.get(processId)!.push(activeAfter);
|
||||
} else {
|
||||
this._readyThreadQueues.get(processId)!.unshift(activeAfter);
|
||||
}
|
||||
this._activeThreads.set(processId, null);
|
||||
}
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
} else if (scheduler.isQuantumExpired(activeThread)) {
|
||||
// ── Quantum intra-processo expirou (RR): rotaciona thread ────────────
|
||||
this._transitionThread(activeThread, "Ready");
|
||||
this._readyThreadQueues.get(processId)!.push(activeThread); // reinserido no fim (FIFO)
|
||||
this._activeThreads.set(processId, null);
|
||||
|
||||
// Seleciona próxima thread — processo permanece na CPU
|
||||
this._dispatchThreadForProcess(cpu);
|
||||
|
||||
// Se não há próxima thread pronta, processo aguarda
|
||||
if (this._activeThreads.get(processId) === null) {
|
||||
this._transition(cpu, "Waiting");
|
||||
if (!this._ioQueue.includes(cpu)) this._ioQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
} else if (this._algorithm.isQuantumExpired(cpu, this._config)) {
|
||||
// ── Quantum de processo expirou: devolve processo à fila global ───────
|
||||
// Thread volta à fila intra-processo
|
||||
this._transitionThread(activeThread, "Ready");
|
||||
if (policy === "RR") {
|
||||
this._readyThreadQueues.get(processId)!.push(activeThread);
|
||||
} else {
|
||||
this._readyThreadQueues.get(processId)!.unshift(activeThread);
|
||||
}
|
||||
this._activeThreads.set(processId, null);
|
||||
this._transition(cpu, "Ready");
|
||||
this._readyQueue.push(cpu);
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trata conclusão do burst de CPU de uma thread específica.
|
||||
* Decide o próximo estado da thread e do processo conforme o modelo.
|
||||
*/
|
||||
private _handleThreadBurstComplete(
|
||||
processRt: ProcessRuntime,
|
||||
thread: ThreadRuntime,
|
||||
process: Process,
|
||||
): void {
|
||||
const processId = process.id;
|
||||
const model = process.threadModel ?? "ONE_TO_ONE";
|
||||
const threadDef = process.threads!.find((t) => t.tid === thread.threadId)!;
|
||||
|
||||
// Avança ao próximo burst da thread
|
||||
thread.burstIndex++;
|
||||
const nextBurst = threadDef.bursts[thread.burstIndex];
|
||||
thread.quantumRemaining = 1; // sentinela anti-falsa-demoção
|
||||
|
||||
if (nextBurst !== undefined && nextBurst.type === "io") {
|
||||
// ── Thread vai para I/O ───────────────────────────────────────────────
|
||||
thread.remainingBurst = nextBurst.duration;
|
||||
this._transitionThread(thread, "Waiting");
|
||||
this._threadIoQueues.get(processId)!.push(thread);
|
||||
|
||||
// Para MANY_TO_MANY: libera slot de kernel
|
||||
if (model === "MANY_TO_MANY") {
|
||||
this._releaseKernelSlot(processId);
|
||||
}
|
||||
|
||||
if (model === "MANY_TO_ONE") {
|
||||
// Kernel thread bloqueado → bloqueia todo o processo
|
||||
this._transition(processRt, "Waiting");
|
||||
this._ioQueue.push(processRt);
|
||||
this._cpuProcess = null;
|
||||
} else {
|
||||
// ONE_TO_ONE / MANY_TO_MANY: verifica se há outra thread pronta
|
||||
this._trySelectNextThreadOrBlock(processRt, process);
|
||||
}
|
||||
} else if (nextBurst !== undefined && nextBurst.type === "cpu") {
|
||||
// ── Thread tem próximo burst de CPU ───────────────────────────────────
|
||||
thread.remainingBurst = nextBurst.duration;
|
||||
this._transitionThread(thread, "Ready");
|
||||
this._readyThreadQueues.get(processId)!.push(thread);
|
||||
// Processo permanece na CPU; seleciona próxima thread
|
||||
this._dispatchThreadForProcess(processRt);
|
||||
} else {
|
||||
// ── Thread terminada ──────────────────────────────────────────────────
|
||||
this._transitionThread(thread, "Terminated");
|
||||
thread.finishTick = this._tick + 1;
|
||||
|
||||
// Para MANY_TO_MANY: libera slot de kernel
|
||||
if (model === "MANY_TO_MANY") {
|
||||
this._releaseKernelSlot(processId);
|
||||
}
|
||||
|
||||
// Verifica se processo deve terminar ou continuar com outra thread
|
||||
if (this._allThreadsTerminated(processId)) {
|
||||
this._transition(processRt, "Terminated");
|
||||
processRt.finishTick = this._tick + 1;
|
||||
this._cpuProcess = null;
|
||||
} else if (model === "MANY_TO_ONE") {
|
||||
// Seleciona próxima thread pronta (se houver)
|
||||
this._trySelectNextThreadOrBlock(processRt, process);
|
||||
} else {
|
||||
this._trySelectNextThreadOrBlock(processRt, process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenta selecionar a próxima thread pronta para o processo.
|
||||
* Se não houver threads prontas e há threads em I/O: processo aguarda.
|
||||
* Se não houver threads prontas e todas terminadas: processo termina.
|
||||
*/
|
||||
private _trySelectNextThreadOrBlock(
|
||||
processRt: ProcessRuntime,
|
||||
process: Process,
|
||||
): void {
|
||||
const processId = process.id;
|
||||
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
|
||||
|
||||
if (readyThreadQueue.length > 0) {
|
||||
// Há threads prontas: seleciona a próxima
|
||||
this._dispatchThreadForProcess(processRt);
|
||||
} else if (this._allThreadsTerminated(processId)) {
|
||||
this._transition(processRt, "Terminated");
|
||||
processRt.finishTick = this._tick + 1;
|
||||
this._cpuProcess = null;
|
||||
} else {
|
||||
// Todas as threads em I/O: processo aguarda
|
||||
this._transition(processRt, "Waiting");
|
||||
if (!this._ioQueue.includes(processRt)) {
|
||||
this._ioQueue.push(processRt);
|
||||
}
|
||||
this._cpuProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerencia conclusão de bursts de I/O para threads.
|
||||
*/
|
||||
private _handleThreadIoCompletions(): void {
|
||||
for (const [processId, threadIoQueue] of this._threadIoQueues) {
|
||||
const processRt = this._runtimes.get(processId)!;
|
||||
const process = this._processes.find((p) => p.id === processId)!;
|
||||
const model = process.threadModel ?? "ONE_TO_ONE";
|
||||
|
||||
const remaining: ThreadRuntime[] = [];
|
||||
|
||||
for (const trt of threadIoQueue) {
|
||||
if (trt.remainingBurst === 0) {
|
||||
// ── I/O da thread concluído ────────────────────────────────────────
|
||||
const threadDef = process.threads!.find((t) => t.tid === trt.threadId)!;
|
||||
trt.burstIndex++;
|
||||
const nextBurst = threadDef.bursts[trt.burstIndex];
|
||||
trt.quantumRemaining = 1; // sentinela
|
||||
|
||||
if (nextBurst !== undefined && nextBurst.type === "cpu") {
|
||||
trt.remainingBurst = nextBurst.duration;
|
||||
|
||||
if (model === "MANY_TO_MANY") {
|
||||
// Tenta adquirir slot de kernel
|
||||
const slotsAvail = this._kernelSlotsAvailable.get(processId) ?? 0;
|
||||
if (slotsAvail > 0) {
|
||||
this._kernelSlotsAvailable.set(processId, slotsAvail - 1);
|
||||
trt.kernelWaiting = false;
|
||||
this._transitionThread(trt, "Ready");
|
||||
this._readyThreadQueues.get(processId)!.push(trt);
|
||||
} else {
|
||||
// Sem slot: vai para kernel wait queue
|
||||
trt.kernelWaiting = true;
|
||||
this._kernelWaitQueues.get(processId)!.push(trt);
|
||||
}
|
||||
} else {
|
||||
this._transitionThread(trt, "Ready");
|
||||
this._readyThreadQueues.get(processId)!.push(trt);
|
||||
}
|
||||
} else {
|
||||
// Thread termina após I/O (ou sem próximo burst)
|
||||
this._transitionThread(trt, "Terminated");
|
||||
trt.finishTick = this._tick + 1;
|
||||
if (model === "MANY_TO_MANY") {
|
||||
// Não havia slot em uso durante I/O; nada a liberar
|
||||
}
|
||||
}
|
||||
|
||||
// Decide estado do processo com base no modelo
|
||||
this._updateProcessStateAfterThreadIo(processRt, process, model);
|
||||
} else {
|
||||
remaining.push(trt);
|
||||
}
|
||||
}
|
||||
|
||||
this._threadIoQueues.set(processId, remaining);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza o estado do processo após conclusão de I/O de uma thread.
|
||||
*/
|
||||
private _updateProcessStateAfterThreadIo(
|
||||
processRt: ProcessRuntime,
|
||||
process: Process,
|
||||
model: string,
|
||||
): void {
|
||||
const processId = process.id;
|
||||
|
||||
if (model === "MANY_TO_ONE") {
|
||||
// O processo estava bloqueado (Waiting) devido ao I/O da thread
|
||||
// Agora a thread está pronta → processo volta para Ready
|
||||
if (processRt.state === "Waiting") {
|
||||
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
|
||||
this._transition(processRt, "Ready");
|
||||
this._readyQueue.push(processRt);
|
||||
this._activeThreads.set(processId, null); // será selecionada no dispatch
|
||||
}
|
||||
} else {
|
||||
// ONE_TO_ONE / MANY_TO_MANY
|
||||
if (processRt.state === "Waiting") {
|
||||
// Processo estava aguardando porque todas as threads estavam em I/O
|
||||
// Verifica se alguma thread ficou pronta
|
||||
const readyThreadQueue = this._readyThreadQueues.get(processId)!;
|
||||
if (readyThreadQueue.length > 0) {
|
||||
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
|
||||
this._transition(processRt, "Ready");
|
||||
this._readyQueue.push(processRt);
|
||||
}
|
||||
}
|
||||
// Se processo está Running: a thread retornou; será selecionada na próxima rodada
|
||||
// Se processo está Ready: a thread apenas entra na fila intra-processo
|
||||
}
|
||||
|
||||
// Verifica se todas as threads terminaram
|
||||
if (this._allThreadsTerminated(processId) && processRt.state !== "Terminated") {
|
||||
this._ioQueue = this._ioQueue.filter((r) => r.processId !== processId);
|
||||
this._readyQueue = this._readyQueue.filter((r) => r.processId !== processId);
|
||||
this._transition(processRt, "Terminated");
|
||||
processRt.finishTick = this._tick + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MANY_TO_MANY — gerenciamento de slots de kernel ─────────────────────
|
||||
|
||||
/**
|
||||
* Libera um slot de kernel e tenta promover a próxima thread da kernel wait queue.
|
||||
*/
|
||||
private _releaseKernelSlot(processId: string): void {
|
||||
const kwQueue = this._kernelWaitQueues.get(processId)!;
|
||||
if (kwQueue.length > 0) {
|
||||
// Repassa o slot diretamente para a próxima thread aguardando
|
||||
const next = kwQueue.shift()!;
|
||||
next.kernelWaiting = false;
|
||||
this._transitionThread(next, "Ready");
|
||||
this._readyThreadQueues.get(processId)!.push(next);
|
||||
} else {
|
||||
this._kernelSlotsAvailable.set(
|
||||
processId,
|
||||
(this._kernelSlotsAvailable.get(processId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers internos ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aplica uma transição de estado a um runtime, atualizando `state`.
|
||||
* Centraliza todas as transições para facilitar rastreamento e depuração.
|
||||
*/
|
||||
private _transition(rt: ProcessRuntime, state: StateLabel): void {
|
||||
rt.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avança o índice de burst de um runtime e atualiza `remainingBurst`
|
||||
* com a duração do próximo burst. Se não houver próximo burst, `remainingBurst` é 0.
|
||||
*/
|
||||
private _transitionThread(trt: ThreadRuntime, state: StateLabel): void {
|
||||
trt.state = state;
|
||||
}
|
||||
|
||||
private _advanceBurst(rt: ProcessRuntime): void {
|
||||
rt.burstIndex++;
|
||||
const process = this._processes.find((p) => p.id === rt.processId)!;
|
||||
@@ -346,45 +839,36 @@ export class SimulationEngine {
|
||||
rt.remainingBurst = nextBurst !== undefined ? nextBurst.duration : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captura o estado instantâneo de todos os processos no tick atual.
|
||||
* Se um context switch estiver em andamento, `cpuProcess` é null e
|
||||
* `ctxSwitchForProcess` identifica o processo prestes a executar.
|
||||
*/
|
||||
private _snapshot(): SimTick {
|
||||
const states: Record<string, StateLabel> = {};
|
||||
for (const [id, rt] of this._runtimes) {
|
||||
states[id] = rt.state;
|
||||
private _hasThreads(processId: string): boolean {
|
||||
const p = this._processes.find((proc) => proc.id === processId);
|
||||
return (p?.threads?.length ?? 0) > 0;
|
||||
}
|
||||
const contextSwitching = this._contextSwitchRemaining > 0;
|
||||
return {
|
||||
tick: this._tick,
|
||||
cpuProcess: contextSwitching ? null : (this._cpuProcess?.processId ?? null),
|
||||
ioProcesses: this._ioQueue.map((rt) => rt.processId),
|
||||
readyQueue: this._readyQueue.map((rt) => rt.processId),
|
||||
states,
|
||||
contextSwitching,
|
||||
ctxSwitchForProcess: contextSwitching
|
||||
? (this._readyQueue[0]?.processId ?? null)
|
||||
: null,
|
||||
};
|
||||
|
||||
private _allThreadsTerminated(processId: string): boolean {
|
||||
const threadMap = this._threadRuntimes.get(processId);
|
||||
if (!threadMap) return true;
|
||||
for (const trt of threadMap.values()) {
|
||||
if (trt.state !== "Terminated") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Inicialização ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Inicializa o estado de runtime de um processo a partir de sua definição.
|
||||
* O primeiro burst deve ser do tipo "cpu"; caso contrário, um erro é lançado.
|
||||
*/
|
||||
private static _initRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
||||
private static _initProcessRuntime(p: Process, config: SchedulerConfig): ProcessRuntime {
|
||||
const isThreaded = (p.threads?.length ?? 0) > 0;
|
||||
|
||||
if (!isThreaded) {
|
||||
const firstBurst = p.bursts[0];
|
||||
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||
throw new Error(`Process ${p.id}: first burst must be a CPU burst`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
processId: p.id,
|
||||
state: "New",
|
||||
remainingBurst: firstBurst.duration,
|
||||
remainingBurst: isThreaded ? 1 : (p.bursts[0]?.duration ?? 0),
|
||||
burstIndex: 0,
|
||||
arrivalTick: p.arrivalTime,
|
||||
startTick: null,
|
||||
@@ -397,4 +881,68 @@ export class SimulationEngine {
|
||||
currentPriority: p.priority,
|
||||
};
|
||||
}
|
||||
|
||||
private _initThreadStructures(p: Process): void {
|
||||
const processId = p.id;
|
||||
const model = p.threadModel ?? "ONE_TO_ONE";
|
||||
const policy = p.threadSchedulingPolicy ?? "FCFS";
|
||||
const quantum = p.threadQuantum ?? 2;
|
||||
const threads = p.threads!;
|
||||
|
||||
// Criação do mapa de runtimes de threads
|
||||
const threadMap = new Map<string, ThreadRuntime>(
|
||||
threads.map((t) => [
|
||||
t.tid,
|
||||
SimulationEngine._initThreadRuntime(t, processId, p.arrivalTime, quantum),
|
||||
]),
|
||||
);
|
||||
this._threadRuntimes.set(processId, threadMap);
|
||||
|
||||
// Escalonador intra-processo
|
||||
this._threadSchedulers.set(processId, new ThreadScheduler(policy, quantum));
|
||||
|
||||
// Estruturas de estado
|
||||
this._activeThreads.set(processId, null);
|
||||
this._threadIoQueues.set(processId, []);
|
||||
this._readyThreadQueues.set(processId, []);
|
||||
|
||||
// MANY_TO_MANY: inicializa slots de kernel
|
||||
if (model === "MANY_TO_MANY") {
|
||||
const M =
|
||||
p.kernelThreadCount ??
|
||||
Math.max(1, Math.ceil(threads.length / 2));
|
||||
this._kernelSlotsAvailable.set(processId, M);
|
||||
this._kernelWaitQueues.set(processId, []);
|
||||
} else {
|
||||
this._kernelWaitQueues.set(processId, []);
|
||||
}
|
||||
}
|
||||
|
||||
private static _initThreadRuntime(
|
||||
t: Thread,
|
||||
processId: string,
|
||||
arrivalTick: number,
|
||||
quantum: number,
|
||||
): ThreadRuntime {
|
||||
const firstBurst = t.bursts[0];
|
||||
if (firstBurst === undefined || firstBurst.type !== "cpu") {
|
||||
throw new Error(`Thread ${t.tid}: first burst must be a CPU burst`);
|
||||
}
|
||||
return {
|
||||
threadId: t.tid,
|
||||
processId,
|
||||
state: "New",
|
||||
remainingBurst: firstBurst.duration,
|
||||
burstIndex: 0,
|
||||
arrivalTick,
|
||||
startTick: null,
|
||||
finishTick: null,
|
||||
waitingTime: 0,
|
||||
cpuTime: 0,
|
||||
ioTime: 0,
|
||||
responseTime: null,
|
||||
quantumRemaining: quantum,
|
||||
currentPriority: t.priority ?? 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import type {
|
||||
GlobalMetrics,
|
||||
Process,
|
||||
ProcessMetrics,
|
||||
ProcessRuntime,
|
||||
Thread,
|
||||
ThreadMetrics,
|
||||
ThreadRuntime,
|
||||
} 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)
|
||||
// Used in the process list preview badge.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MlqQueueDef, Process, SchedulerConfig } from "./types";
|
||||
// Thread preset IDs are prefixed with "t_" to avoid conflicts
|
||||
|
||||
export interface Preset {
|
||||
name: string;
|
||||
@@ -127,4 +128,319 @@ export const presets: Preset[] = [
|
||||
],
|
||||
config: defaultConfig({ algorithm: "MULTILEVEL", quantum: 2, mlqQueues: DEFAULT_MLQ_QUEUES }),
|
||||
},
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// PRESETS DE THREADS — demonstram os três modelos e conceitos de concorrência
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
name: "many_to_one",
|
||||
description:
|
||||
"MANY_TO_ONE: 2 processos com 3 threads cada. Uma thread em I/O bloqueia todo o processo — observe como o processo fica Waiting enquanto as outras threads ficam ociosas.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "Srv-A",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 2,
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "A-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 4 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "A-T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "A-T3",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
id: "t_p2",
|
||||
name: "Srv-B",
|
||||
arrivalTime: 2,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p2_t1",
|
||||
name: "B-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p2_t2",
|
||||
name: "B-T2",
|
||||
bursts: [{ type: "cpu", duration: 5 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "one_to_one",
|
||||
description:
|
||||
"ONE_TO_ONE: mesmo cenário do Many-to-One mas com kernel threads independentes. Threads em I/O não bloqueiam o processo — compare o throughput com o preset many_to_one.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "Srv-A",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 2,
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "A-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 4 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "A-T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "A-T3",
|
||||
bursts: [{ type: "cpu", duration: 4 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
id: "t_p2",
|
||||
name: "Srv-B",
|
||||
arrivalTime: 2,
|
||||
priority: 5,
|
||||
color: "#81C784",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "FCFS",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p2_t1",
|
||||
name: "B-T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p2_t2",
|
||||
name: "B-T2",
|
||||
bursts: [{ type: "cpu", duration: 5 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "many_to_many",
|
||||
description:
|
||||
"MANY_TO_MANY: processo com 4 threads de usuário mas apenas 2 kernel threads (M=2). Observe threads em KernelWait aguardando slot disponível.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#CE93D8",
|
||||
bursts: [],
|
||||
threadModel: "MANY_TO_MANY",
|
||||
kernelThreadCount: 2,
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "Worker1",
|
||||
priority: 1,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "Worker2",
|
||||
priority: 2,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "Worker3",
|
||||
priority: 3,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 4 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t4",
|
||||
name: "Worker4",
|
||||
priority: 4,
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "FCFS" }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "thread_starvation",
|
||||
description:
|
||||
"Starvation de thread: processo com 3 threads em política Priority. A thread de baixa prioridade (T3) é indefinidamente preterida pelas de alta prioridade.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "App",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#F06292",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "PRIORITY",
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "High-1",
|
||||
priority: 1,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "High-2",
|
||||
priority: 2,
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 1 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t3",
|
||||
name: "Low",
|
||||
priority: 10,
|
||||
bursts: [{ type: "cpu", duration: 3 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "FCFS" }),
|
||||
},
|
||||
|
||||
{
|
||||
name: "threads_vs_processes",
|
||||
description:
|
||||
"Threads vs Processos: P1 usa 2 threads ONE_TO_ONE para uma tarefa. P2 faz o mesmo trabalho como processo único. Compare turnaround e throughput.",
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
id: "t_p1",
|
||||
name: "Multi-T",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#4FC3F7",
|
||||
bursts: [],
|
||||
threadModel: "ONE_TO_ONE",
|
||||
threadSchedulingPolicy: "RR",
|
||||
threadQuantum: 3,
|
||||
threads: [
|
||||
{
|
||||
tid: "t_p1_t1",
|
||||
name: "T1",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 3 },
|
||||
{ type: "io", duration: 2 },
|
||||
{ type: "cpu", duration: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tid: "t_p1_t2",
|
||||
name: "T2",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 2 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
id: "t_p2",
|
||||
name: "Single-P",
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: "#FFB74D",
|
||||
bursts: [
|
||||
{ type: "cpu", duration: 5 },
|
||||
{ type: "io", duration: 3 },
|
||||
{ type: "cpu", duration: 5 },
|
||||
],
|
||||
},
|
||||
],
|
||||
config: defaultConfig({ algorithm: "RR", quantum: 4 }),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,74 @@
|
||||
// ============================================================
|
||||
// ThreadScheduler — Escalonamento intra-processo de threads
|
||||
// ============================================================
|
||||
//
|
||||
// Responsável por selecionar qual thread executa dentro de um processo
|
||||
// quando ele recebe tempo de CPU. Suporta três políticas:
|
||||
//
|
||||
// FCFS — FIFO estrito por ordem de chegada (arrivalTick)
|
||||
// RR — Round-Robin com quantum intra-processo
|
||||
// PRIORITY — Prioridade da thread (currentPriority, menor = maior prioridade)
|
||||
|
||||
import type { IntraProcessPolicy, ThreadRuntime } from "./types";
|
||||
|
||||
export class ThreadScheduler {
|
||||
readonly policy: IntraProcessPolicy;
|
||||
readonly quantum: number;
|
||||
|
||||
constructor(policy: IntraProcessPolicy = "FCFS", quantum = 2) {
|
||||
this.policy = policy;
|
||||
this.quantum = quantum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seleciona a próxima thread a executar a partir da fila de prontas.
|
||||
* Não modifica o array — o engine remove a thread selecionada.
|
||||
*/
|
||||
select(readyThreads: readonly ThreadRuntime[]): ThreadRuntime | null {
|
||||
if (readyThreads.length === 0) return null;
|
||||
|
||||
switch (this.policy) {
|
||||
case "FCFS":
|
||||
// Primeira a chegar, primeira a ser servida
|
||||
return [...readyThreads].sort(
|
||||
(a, b) =>
|
||||
a.arrivalTick - b.arrivalTick ||
|
||||
a.threadId.localeCompare(b.threadId),
|
||||
)[0] ?? null;
|
||||
|
||||
case "RR":
|
||||
// Ordem de fila (o engine mantém a ordem FIFO ao reinserir)
|
||||
return readyThreads[0] ?? null;
|
||||
|
||||
case "PRIORITY":
|
||||
// Menor currentPriority = maior prioridade; desempate por arrivalTick
|
||||
return [...readyThreads].sort(
|
||||
(a, b) =>
|
||||
a.currentPriority - b.currentPriority ||
|
||||
a.arrivalTick - b.arrivalTick ||
|
||||
a.threadId.localeCompare(b.threadId),
|
||||
)[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o quantum intra-processo da thread expirou.
|
||||
* Apenas relevante para política RR.
|
||||
*/
|
||||
isQuantumExpired(thread: ThreadRuntime): boolean {
|
||||
return this.policy === "RR" && thread.quantumRemaining <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Para política PRIORITY: indica se a thread candidata deve preemptar a atual.
|
||||
*/
|
||||
shouldPreemptThread(
|
||||
current: ThreadRuntime,
|
||||
candidate: ThreadRuntime,
|
||||
): boolean {
|
||||
return (
|
||||
this.policy === "PRIORITY" &&
|
||||
candidate.currentPriority < current.currentPriority
|
||||
);
|
||||
}
|
||||
}
|
||||
+86
-2
@@ -11,7 +11,36 @@ export type SchedulingAlgorithm =
|
||||
| "PRIORITY_P"
|
||||
| "PRIORITY_RR"
|
||||
| "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 ------------------
|
||||
|
||||
@@ -27,7 +56,13 @@ export interface Process {
|
||||
arrivalTime: number; // tick of arrival (≥ 0)
|
||||
priority: number; // 1 (highest) – 10 (lowest); default 5
|
||||
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 --------------------
|
||||
@@ -39,6 +74,10 @@ export interface MlqQueueDef {
|
||||
quantum: number; // used only when algorithm === "RR"
|
||||
}
|
||||
|
||||
export interface MlfqLevelDef {
|
||||
quantum: number; // time slice for this level (ticks, >= 1)
|
||||
}
|
||||
|
||||
export interface SchedulerConfig {
|
||||
algorithm: SchedulingAlgorithm;
|
||||
quantum: number; // for RR (default 2)
|
||||
@@ -47,6 +86,8 @@ export interface SchedulerConfig {
|
||||
agingEnabled: boolean; // prevent starvation in Priority scheduling
|
||||
agingInterval: number; // ticks in Ready before priority-- (min 1)
|
||||
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 ------------------------------
|
||||
@@ -58,6 +99,28 @@ export type StateLabel =
|
||||
| "Waiting" // executing I/O burst
|
||||
| "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 {
|
||||
// ---- spec fields ----
|
||||
processId: string;
|
||||
@@ -74,6 +137,7 @@ export interface ProcessRuntime {
|
||||
// ---- engine-internal fields ----
|
||||
quantumRemaining: number; // remaining quantum for RR; reset on dispatch
|
||||
currentPriority: number; // mutable via aging; starts = Process.priority
|
||||
mlfqLevel?: number; // current MLFQ level (0 = highest); undefined for non-MLFQ runs
|
||||
}
|
||||
|
||||
// --------------- Tick snapshot ------------------------------
|
||||
@@ -86,10 +150,30 @@ export interface SimTick {
|
||||
states: Record<string, StateLabel>; // all processId → state
|
||||
contextSwitching: boolean; // true during context-switch overhead
|
||||
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 ------------------------------------
|
||||
|
||||
/** 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 {
|
||||
processId: string;
|
||||
arrivalTime: number;
|
||||
|
||||
+51
-6
@@ -1,5 +1,4 @@
|
||||
import type { ProcessMetrics } from "@core/types";
|
||||
import type { Process } from "@core/types";
|
||||
import type { ProcessMetrics, ThreadMetrics, Process } from "@core/types";
|
||||
|
||||
interface ProcessMap {
|
||||
[id: string]: Process;
|
||||
@@ -16,8 +15,13 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
|
||||
}
|
||||
|
||||
export function useExport() {
|
||||
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
|
||||
const headers = [
|
||||
const exportCSV = (
|
||||
metrics: ProcessMetrics[],
|
||||
processMap: ProcessMap,
|
||||
threadMetrics?: ThreadMetrics[],
|
||||
) => {
|
||||
// ── Seção de processos ─────────────────────────────────────────────────
|
||||
const processHeaders = [
|
||||
"PID",
|
||||
"Process",
|
||||
"Priority",
|
||||
@@ -33,7 +37,7 @@ export function useExport() {
|
||||
"Bound",
|
||||
];
|
||||
|
||||
const rows = metrics.map((m) => {
|
||||
const processRows = metrics.map((m) => {
|
||||
const proc = processMap[m.processId];
|
||||
const pid = proc?.pid ?? "";
|
||||
const name = proc?.name ?? m.processId;
|
||||
@@ -55,7 +59,48 @@ export function useExport() {
|
||||
].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;");
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user