Implement Priority Round Robin Scheduling Algorithm with Tests

- Added `priority_rr` algorithm to handle scheduling based on priority and round-robin principles.
- Created unit tests for `select()`, `shouldPreempt()`, and `isQuantumExpired()` functions to ensure correct behavior.
- Developed integration tests to validate the scheduling logic under various scenarios, including preemption and round-robin execution.
- Introduced `useExport` hook for exporting process metrics to CSV format.
- Added `useGanttScroll` hook to manage smooth scrolling of the Gantt chart during simulation.
This commit is contained in:
Lucas Rayan committed 2026-03-19 16:45:05 -03:00
1 parent 083fc0027e
commit 51a8a6fa40
38 files changed
+2530 -45

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
node_modules
dist/
.git
.gitignore
*.md
+13
View File
@@ -0,0 +1,13 @@
# ── Production image ──
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 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
CMD ["nginx", "-g", "daemon off;"]
BIN
View File
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
services:
pss:
build: .
ports:
- "${PORT:-80}:80"
restart: unless-stopped
+22
View File
@@ -0,0 +1,22 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Cache para assets com hash no nome (gerados pelo Vite)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA: todas as rotas vão para index.html
location / {
try_files $uri $uri/ /index.html;
}
}
+401 -17
View File
@@ -13,6 +13,7 @@
"lucide-react": "^0.577.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^3.8.0",
"tailwind-merge": "^3.5.0",
"zustand": "^5.0.12"
},
@@ -146,7 +147,6 @@
"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,7 +519,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -568,7 +567,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -1122,6 +1120,42 @@
"node": ">=14"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer": {
"version": "11.1.4",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1479,6 +1513,18 @@
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
@@ -1846,7 +1892,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -1904,6 +1951,69 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -1924,7 +2034,6 @@
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -1942,7 +2051,6 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -1954,11 +2062,16 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -2260,7 +2373,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2412,6 +2524,127 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
@@ -2451,6 +2684,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -2486,7 +2725,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
@@ -2543,6 +2783,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/es-toolkit": {
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz",
"integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -2605,6 +2855,12 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -2767,6 +3023,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@@ -2777,6 +3043,15 @@
"node": ">=8"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -2893,7 +3168,6 @@
"integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@asamuzakjp/css-color": "^5.0.1",
"@asamuzakjp/dom-selector": "^7.0.2",
@@ -3270,6 +3544,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -3485,7 +3760,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3528,6 +3802,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -3543,6 +3818,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -3553,6 +3829,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -3575,7 +3852,6 @@
"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"
},
@@ -3588,7 +3864,6 @@
"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"
@@ -3601,8 +3876,31 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.2.0",
"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",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
@@ -3614,6 +3912,36 @@
"node": ">=0.10.0"
}
},
"node_modules/recharts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
"integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
"license": "MIT",
"workspaces": [
"www"
],
"dependencies": {
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -3628,6 +3956,21 @@
"node": ">=8"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -3638,6 +3981,12 @@
"node": ">=0.10.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
@@ -3985,6 +4334,12 @@
"node": ">=18"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -4154,13 +4509,43 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -4259,7 +4644,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
+2
View File
@@ -6,6 +6,7 @@
"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",
@@ -16,6 +17,7 @@
"lucide-react": "^0.577.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^3.8.0",
"tailwind-merge": "^3.5.0",
"zustand": "^5.0.12"
},
+2
View File
@@ -1,6 +1,8 @@
import AppLayout from './components/Layout/AppLayout';
import { useSimulation } from './hooks/useSimulation';
function App() {
useSimulation();
return (
<AppLayout />
);
+53
View File
@@ -0,0 +1,53 @@
import React from "react";
import type { Process, SimTick, StateLabel } from "@core/types";
import { CELL_H, CELL_W } from "./constants";
// ── colour map ──────────────────────────────────────────────
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)]",
};
// ── props ────────────────────────────────────────────────────
interface GanttCellProps {
tick: SimTick;
process: Process;
state: StateLabel;
isCtxSwitch: boolean;
onMouseEnter: (e: React.MouseEvent, tick: SimTick, process: Process, state: StateLabel, isCtxSwitch: boolean) => void;
onMouseLeave: () => void;
}
// ── component ───────────────────────────────────────────────
const GanttCell = React.memo(function GanttCell({
tick,
process,
state,
isCtxSwitch,
onMouseEnter,
onMouseLeave,
}: GanttCellProps) {
const bg = isCtxSwitch
? "bg-[var(--color-state-ctx)]"
: STATE_CLASS[state];
// Running cells get a coloured left border using the process colour
const borderStyle =
!isCtxSwitch && state === "Running"
? { borderLeft: `3px solid ${process.color}` }
: undefined;
return (
<div
className={`${bg} shrink-0 opacity-90 hover:opacity-100 transition-opacity cursor-default`}
style={{ width: CELL_W, height: CELL_H, ...borderStyle }}
onMouseEnter={(e) => onMouseEnter(e, tick, process, state, isCtxSwitch)}
onMouseLeave={onMouseLeave}
/>
);
});
export default GanttCell;
+113
View File
@@ -0,0 +1,113 @@
import { useCallback, useRef, useState } from "react";
import { useSimulationStore } from "@store/simulationStore";
import { useProcessStore } from "@store/processStore";
import { useGanttScroll } from "@hooks/useGanttScroll";
import GanttRow from "./GanttRow";
import GanttLegend from "./GanttLegend";
import GanttTooltip, { type TooltipData } from "./GanttTooltip";
import { CELL_H, CELL_W } from "./constants";
// ── GanttChart ────────────────────────────────────────────────
const GanttChart = () => {
const ticks = useSimulationStore((s) => s.ticks);
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 }}>
Inicie a simulação para ver o diagrama de Gantt
</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"
>
<div style={{ width: totalWidth, position: "relative" }}>
{/* 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 }} />
{tickNumbers.map((t) => (
<div
key={t}
className="shrink-0 text-center text-[9px] text-slate-500 font-mono select-none"
style={{ width: CELL_W, height: 16, lineHeight: "16px" }}
>
{t % 5 === 0 ? t : ""}
</div>
))}
</div>
{/* Process 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) */}
<div
className="sticky left-0 z-20 bg-slate-900 border-r border-slate-700 px-2 flex items-center shrink-0"
style={{ width: 48, height: CELL_H }}
>
<span className="text-[10px] font-bold truncate" style={{ color: process.color }}>
{process.name}
</span>
</div>
<GanttRow
process={process}
ticks={ticks}
onCellMouseEnter={setTooltip}
onCellMouseLeave={handleMouseLeave}
/>
</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 }}
/>
)}
</div>
</div>
</div>
{/* ── process name labels (sticky left, overlay) ── */}
{/* Rendered OUTSIDE the scrollable div so they stay fixed on the left */}
{/* We achieve this with a separate absolute-positioned column */}
{/* ── legend ── */}
<GanttLegend />
{/* ── tooltip ── */}
{tooltip && <GanttTooltip data={tooltip} />}
</div>
);
};
export default GanttChart;
+25
View File
@@ -0,0 +1,25 @@
// Legend items ordered as they appear in a typical lifecycle
const ITEMS: { label: string; cssVar: string; textClass: string }[] = [
{ label: "New", cssVar: "var(--color-state-new)", textClass: "text-white" },
{ label: "Ready", cssVar: "var(--color-state-ready)", textClass: "text-slate-900" },
{ label: "Running", cssVar: "var(--color-state-running)", textClass: "text-slate-900" },
{ label: "Waiting (I/O)", cssVar: "var(--color-state-waiting)", textClass: "text-slate-900" },
{ label: "Terminated", cssVar: "var(--color-state-terminated)", textClass: "text-slate-900" },
{ label: "Context Switch",cssVar: "var(--color-state-ctx)", textClass: "text-white" },
];
const GanttLegend = () => (
<div className="flex flex-wrap gap-x-4 gap-y-2 px-4 py-3 border-t border-slate-700/60">
{ITEMS.map(({ label, cssVar, textClass }) => (
<div key={label} className="flex items-center gap-1.5">
<span
className={`inline-block rounded-sm ${textClass}`}
style={{ width: 14, height: 14, background: cssVar, flexShrink: 0 }}
/>
<span className="text-xs text-slate-400">{label}</span>
</div>
))}
</div>
);
export default GanttLegend;
+51
View File
@@ -0,0 +1,51 @@
import React from "react";
import type { Process, SimTick, StateLabel } from "@core/types";
import GanttCell from "./GanttCell";
import { CELL_H, CELL_W } from "./constants";
import type { TooltipData } from "./GanttTooltip";
interface GanttRowProps {
process: Process;
ticks: SimTick[];
onCellMouseEnter: (data: TooltipData) => void;
onCellMouseLeave: () => void;
}
const GanttRow = React.memo(function GanttRow({
process,
ticks,
onCellMouseEnter,
onCellMouseLeave,
}: GanttRowProps) {
return (
<div className="flex" style={{ height: CELL_H }}>
{ticks.map((tick) => {
const state: StateLabel = tick.states[process.id] ?? "New";
// A context-switch tick affects the process currently on CPU
const isCtxSwitch =
tick.contextSwitching && tick.cpuProcess === process.id;
return (
<GanttCell
key={tick.tick}
tick={tick}
process={process}
state={state}
isCtxSwitch={isCtxSwitch}
onMouseEnter={(e, t, p, s, ctx) =>
onCellMouseEnter({ tick: t, process: p, state: s, isCtxSwitch: ctx, x: e.clientX, y: e.clientY })
}
onMouseLeave={onCellMouseLeave}
/>
);
})}
{/* Empty filler so the row is always at least CELL_W wide */}
{ticks.length === 0 && (
<div style={{ width: CELL_W, height: CELL_H }} className="bg-slate-800/40" />
)}
</div>
);
});
export default GanttRow;
@@ -0,0 +1,80 @@
import type { Process, SimTick, StateLabel } from "@core/types";
// ── colour labels ────────────────────────────────────────────
const STATE_LABEL: Record<StateLabel, string> = {
New: "New",
Ready: "Ready",
Running: "Running",
Waiting: "Waiting (I/O)",
Terminated: "Terminated",
};
const STATE_BADGE: Record<StateLabel, string> = {
New: "bg-[var(--color-state-new)] text-white",
Ready: "bg-[var(--color-state-ready)] text-slate-900",
Running: "bg-[var(--color-state-running)] text-slate-900",
Waiting: "bg-[var(--color-state-waiting)] text-slate-900",
Terminated: "bg-[var(--color-state-terminated)] text-slate-900",
};
// ── types ────────────────────────────────────────────────────
export interface TooltipData {
tick: SimTick;
process: Process;
state: StateLabel;
isCtxSwitch: boolean;
x: number;
y: number;
}
interface GanttTooltipProps {
data: TooltipData;
}
// ── component ────────────────────────────────────────────────
const GanttTooltip = ({ data }: GanttTooltipProps) => {
const { tick, process, state, isCtxSwitch, x, y } = data;
// Keep the tooltip fully inside the viewport
const left = Math.min(x + 12, window.innerWidth - 220);
const top = Math.min(y + 12, window.innerHeight - 140);
return (
<div
className="fixed z-50 pointer-events-none bg-slate-800 border border-slate-600 rounded-lg shadow-2xl px-3 py-2.5 text-xs text-slate-100 w-52"
style={{ left, top }}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-sm" style={{ color: process.color }}>
{process.name}
</span>
<span className="text-slate-400 font-mono">tick {tick.tick}</span>
</div>
{/* State badge */}
<div className="mb-1.5">
{isCtxSwitch ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-state-ctx text-white font-medium">
⚠ Context Switch
</span>
) : (
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full font-medium ${STATE_BADGE[state]}`}
>
{STATE_LABEL[state]}
</span>
)}
</div>
{/* Queue context */}
{tick.readyQueue.length > 0 && (
<p className="text-slate-400 text-[10px] mt-1">
Ready queue: {tick.readyQueue.length} process{tick.readyQueue.length !== 1 ? "es" : ""}
</p>
)}
</div>
);
};
export default GanttTooltip;
@@ -0,0 +1,79 @@
import { describe, it, expect, vi } from "vitest";
import { render, fireEvent } from "@testing-library/react";
import GanttCell from "../GanttCell";
import type { Process, SimTick } from "@core/types";
describe("GanttCell", () => {
const mockProcess: Process = {
id: "p1",
name: "P1",
arrivalTime: 0,
priority: 5,
color: "#ff0000",
bursts: [{ type: "cpu", duration: 5 }],
};
const mockTick: SimTick = {
tick: 1,
cpuProcess: "p1",
ioProcesses: [],
readyQueue: [],
states: { p1: "Running" },
contextSwitching: false,
};
const noop = () => {};
it("renders with the correct background color for Running state", () => {
const { container } = render(
<GanttCell
tick={mockTick}
process={mockProcess}
state="Running"
isCtxSwitch={false}
onMouseEnter={noop}
onMouseLeave={noop}
/>
);
const cell = container.firstChild as HTMLElement;
expect(cell.className).toContain("bg-[var(--color-state-running)]");
});
it("renders with the context switch color when isCtxSwitch is true", () => {
const { container } = render(
<GanttCell
tick={mockTick}
process={mockProcess}
state="Running"
isCtxSwitch={true}
onMouseEnter={noop}
onMouseLeave={noop}
/>
);
const cell = container.firstChild as HTMLElement;
expect(cell.className).toContain("bg-[var(--color-state-ctx)]");
});
it("calls onMouseEnter with correct arguments", () => {
const onMouseEnter = vi.fn();
const { container } = render(
<GanttCell
tick={mockTick}
process={mockProcess}
state="Running"
isCtxSwitch={false}
onMouseEnter={onMouseEnter}
onMouseLeave={noop}
/>
);
const cell = container.firstChild as HTMLElement;
fireEvent.mouseEnter(cell);
expect(onMouseEnter).toHaveBeenCalledWith(
expect.anything(),
mockTick,
mockProcess,
"Running",
false
);
});
});
+5
View File
@@ -0,0 +1,5 @@
/** Width of a single tick cell (px) */
export const CELL_W = 28;
/** Height of a single process row (px) */
export const CELL_H = 32;
+150
View File
@@ -0,0 +1,150 @@
import { useState, useRef } from 'react';
import { ChevronDown, Download, Upload, BookOpen } from 'lucide-react';
import { presets } from '@core/presets';
import { useProcessStore } from '../../store/processStore';
import { useSimulationStore } from '../../store/simulationStore';
const AppHeader = () => {
const [presetsOpen, setPresetsOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const loadPreset = useProcessStore(s => s.loadPreset);
const processes = useProcessStore(s => s.processes);
const config = useProcessStore(s => s.config);
const simStatus = useSimulationStore(s => s.status);
const handleLoadPreset = (index: number) => {
const p = presets[index];
if (!p) return;
loadPreset(p.processes, p.config);
setPresetsOpen(false);
};
const handleExportJSON = () => {
const data = JSON.stringify({ processes, config }, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'pss-config.json';
a.click();
URL.revokeObjectURL(url);
};
const handleImportJSON = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const parsed = JSON.parse(ev.target?.result as string);
if (parsed.processes && parsed.config) {
loadPreset(parsed.processes, parsed.config);
}
} catch {
// Silently ignore malformed JSON
}
};
reader.readAsText(file);
// Reset so the same file can be imported again
e.target.value = '';
};
const isSimulationActive = simStatus !== 'idle';
return (
<header className="h-12 flex items-center px-5 gap-4 bg-slate-900 border-b border-slate-700/60 shrink-0 z-30">
{/* Title */}
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent whitespace-nowrap">
Process Scheduler Simulator
</h1>
<div className="flex-1" />
{/* Presets dropdown */}
<div className="relative">
<button
onClick={() => setPresetsOpen(o => !o)}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
aria-label="Carregar cenário pré-definido"
>
<BookOpen size={13} />
Presets
<ChevronDown size={12} className={`transition-transform ${presetsOpen ? 'rotate-180' : ''}`} />
</button>
{presetsOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setPresetsOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden">
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium">
Cenários pré-definidos
</div>
{presets.map((preset, i) => (
<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="text-xs font-medium text-slate-200 group-hover:text-white">
{preset.name}
</div>
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">
{preset.description}
</div>
<div className="text-[10px] text-blue-400 mt-0.5">
{preset.config.algorithm}
</div>
</button>
))}
</div>
</>
)}
</div>
{/* Import JSON */}
<label
htmlFor="import-json"
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'}
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
isSimulationActive
? 'text-slate-600 cursor-not-allowed'
: 'text-slate-300 hover:text-white hover:bg-slate-700'
}`}
aria-label="Importar configuração JSON"
>
<Upload size={13} />
Importar
</label>
<input
id="import-json"
ref={fileInputRef}
type="file"
accept=".json"
className="sr-only"
onChange={handleImportJSON}
disabled={isSimulationActive}
aria-label="Selecionar arquivo JSON para importar"
/>
{/* Export JSON */}
<button
onClick={handleExportJSON}
disabled={processes.length === 0}
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar configuração JSON"
>
<Download size={13} />
Exportar
</button>
</header>
);
};
export default AppHeader;
+7 -3
View File
@@ -1,11 +1,15 @@
import AppHeader from './AppHeader';
import Sidebar from './Sidebar';
import MainArea from './MainArea';
const AppLayout = () => {
return (
<div className="flex h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
<Sidebar />
<MainArea />
<div className="flex flex-col h-screen w-full bg-slate-900 text-slate-200 overflow-hidden font-sans">
<AppHeader />
<div className="flex flex-1 overflow-hidden">
<Sidebar />
<MainArea />
</div>
</div>
);
};
+5 -15
View File
@@ -1,23 +1,13 @@
import ControlBar from '../ControlBar/ControlBar';
import GanttChart from '../GanttChart/GanttChart';
import MetricsPanel from '../MetricsPanel/MetricsPanel';
const MainArea = () => {
return (
<main className="flex-1 flex flex-col h-full bg-slate-900 overflow-hidden relative">
<div className="flex-1 flex flex-col">
<ControlBar />
{/* Placeholder for GanttChart — Phase 4 */}
<div className="border-b border-slate-800 h-80 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm">
[ Gantt Chart — Fase 4 ]
</div>
{/* Placeholder for MetricsPanel — Phase 5 */}
<div className="flex-1 flex items-center justify-center text-slate-600 bg-slate-950 p-6">
<div className="w-full h-full border border-dashed border-slate-700 rounded-xl flex items-center justify-center text-sm">
[ Metrics Panel — Fase 5 ]
</div>
</div>
</div>
<ControlBar />
<GanttChart />
<MetricsPanel />
</main>
);
};
@@ -0,0 +1,83 @@
import type { GlobalMetrics as GlobalMetricsType } from '@core/types';
interface GlobalMetricsProps {
metrics: GlobalMetricsType;
}
interface MetricCardProps {
label: string;
value: string;
sub?: string;
accent?: string;
}
const MetricCard = ({ label, value, sub, accent = 'text-blue-300' }: MetricCardProps) => (
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
<span className={`text-xl font-bold font-mono ${accent}`}>{value}</span>
{sub && <span className="text-[10px] text-slate-500">{sub}</span>}
</div>
);
const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
const {
avgResponseTime,
avgTurnaroundTime,
avgWaitingTime,
cpuThroughput,
cpuUtilization,
totalSimulationTime,
} = metrics;
const utilizationPct = Math.min(100, cpuUtilization);
return (
<div className="p-4 space-y-4">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<MetricCard
label="Avg Response Time"
value={avgResponseTime.toFixed(2)}
sub="unidades de tempo"
accent="text-amber-300"
/>
<MetricCard
label="Avg Turnaround Time"
value={avgTurnaroundTime.toFixed(2)}
sub="unidades de tempo"
accent="text-blue-300"
/>
<MetricCard
label="Avg Waiting Time"
value={avgWaitingTime.toFixed(2)}
sub="unidades de tempo"
accent="text-orange-300"
/>
<MetricCard
label="CPU Throughput"
value={cpuThroughput.toFixed(3)}
sub="processos / u.t."
accent="text-emerald-300"
/>
<MetricCard
label="Total Simulation Time"
value={String(totalSimulationTime)}
sub="ticks"
accent="text-slate-200"
/>
{/* CPU Utilization card with progress bar */}
<div className="bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 flex flex-col gap-1">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">CPU Utilization</span>
<span className="text-xl font-bold font-mono text-violet-300">{utilizationPct.toFixed(1)}%</span>
<div className="mt-1 h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-violet-500 rounded-full transition-all duration-500"
style={{ width: `${utilizationPct}%` }}
/>
</div>
</div>
</div>
</div>
);
};
export default GlobalMetrics;
@@ -0,0 +1,82 @@
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
CartesianGrid,
} from 'recharts';
import type { ProcessMetrics, Process } from '@core/types';
interface MetricsChartProps {
metrics: ProcessMetrics[];
processMap: Record<string, Process>;
}
const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Nenhum dado disponível.
</div>
);
}
const data = metrics.map((m) => ({
name: processMap[m.processId]?.name ?? m.processId,
'Waiting Time': m.waitingTime,
'CPU Time': m.cpuTime,
'I/O Time': m.ioTime,
}));
return (
<div className="p-4">
<ResponsiveContainer width="100%" height={220}>
<BarChart data={data} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
<XAxis
dataKey="name"
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={{ stroke: '#475569' }}
tickLine={false}
/>
<YAxis
tick={{ fill: '#94a3b8', fontSize: 11 }}
axisLine={false}
tickLine={false}
label={{
value: 'ticks',
angle: -90,
position: 'insideLeft',
offset: 10,
style: { fill: '#64748b', fontSize: 10 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '6px',
fontSize: '12px',
color: '#e2e8f0',
}}
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
/>
<Legend
wrapperStyle={{ fontSize: '11px', paddingTop: '8px', color: '#94a3b8' }}
/>
<Bar dataKey="Waiting Time" stackId="a" fill="#f59e0b" radius={[0, 0, 0, 0]} />
<Bar dataKey="CPU Time" stackId="a" fill="#22c55e" radius={[0, 0, 0, 0]} />
<Bar dataKey="I/O Time" stackId="a" fill="#06b6d4" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<p className="text-center text-[10px] text-slate-500 mt-1">
Barras empilhadas = Turnaround Time (Waiting + CPU + I/O)
</p>
</div>
);
};
export default MetricsChart;
@@ -0,0 +1,98 @@
import { useState } from 'react';
import { Download, BarChart2, Table, Globe } from 'lucide-react';
import { computeProcessMetrics, computeGlobalMetrics } 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';
type Tab = 'table' | 'global' | 'chart';
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'table', label: 'Por Processo', icon: <Table size={13} /> },
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> },
];
const MetricsPanel = () => {
const [activeTab, setActiveTab] = useState<Tab>('table');
const engine = useSimulationStore(s => s.engine);
const ticks = useSimulationStore(s => s.ticks);
const status = useSimulationStore(s => s.status);
const processes = useProcessStore(s => s.processes);
const { exportCSV } = useExport();
const hasData = ticks.length > 0;
if (!hasData) {
return (
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
Execute a simulação para ver as métricas.
</div>
);
}
// Derive metrics from engine runtime state
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]));
const canExportCSV = status === 'finished' && processMetrics.length > 0;
return (
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '280px' }}>
{/* 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 => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors ${
activeTab === tab.id
? 'bg-slate-700 text-slate-100 font-medium'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'
}`}
aria-pressed={activeTab === tab.id}
>
{tab.icon}
{tab.label}
</button>
))}
<div className="ml-auto">
<button
onClick={() => exportCSV(processMetrics, processMap)}
disabled={!canExportCSV}
title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como 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"
aria-label="Exportar métricas CSV"
>
<Download size={12} />
CSV
</button>
</div>
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto">
{activeTab === 'table' && (
<MetricsTable metrics={processMetrics} processMap={processMap} />
)}
{activeTab === 'global' && (
<GlobalMetrics metrics={globalMetrics} />
)}
{activeTab === 'chart' && (
<MetricsChart metrics={processMetrics} processMap={processMap} />
)}
</div>
</div>
);
};
export default MetricsPanel;
@@ -0,0 +1,82 @@
import type { ProcessMetrics, Process } from '@core/types';
import Badge from '../shared/Badge';
interface MetricsTableProps {
metrics: ProcessMetrics[];
processMap: Record<string, Process>;
}
function boundVariant(type: ProcessMetrics['boundType']) {
if (type === 'CPU Bound') return 'cpu';
if (type === 'I/O Bound') return 'io';
return 'balanced';
}
const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
Nenhum processo concluído ainda.
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-[700px]">
<thead>
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700">
<th className="text-left px-3 py-2 font-medium">Processo</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">Response</th>
<th className="text-right px-3 py-2 font-medium">Turnaround</th>
<th className="text-right px-3 py-2 font-medium">Waiting</th>
<th className="text-right px-3 py-2 font-medium">CPU Time</th>
<th className="text-right px-3 py-2 font-medium">I/O Time</th>
<th className="text-right px-3 py-2 font-medium">CPU%</th>
<th className="text-center px-3 py-2 font-medium">Bound</th>
</tr>
</thead>
<tbody>
{metrics.map((m, i) => {
const proc = processMap[m.processId];
const name = proc?.name ?? m.processId;
const color = proc?.color ?? '#94a3b8';
return (
<tr
key={m.processId}
className={`border-b border-slate-800 hover:bg-slate-800/60 transition-colors ${i % 2 === 0 ? '' : 'bg-slate-900/30'}`}
>
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<span
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
style={{ backgroundColor: color }}
/>
<span className="font-medium text-slate-200">{name}</span>
</div>
</td>
<td className="text-right px-3 py-2 font-mono">{m.arrivalTime}</td>
<td className="text-right px-3 py-2 font-mono">{m.startTick}</td>
<td className="text-right px-3 py-2 font-mono">{m.finishTick}</td>
<td className="text-right px-3 py-2 font-mono text-amber-300">{m.responseTime}</td>
<td className="text-right px-3 py-2 font-mono text-blue-300">{m.turnaroundTime}</td>
<td className="text-right px-3 py-2 font-mono text-orange-300">{m.waitingTime}</td>
<td className="text-right px-3 py-2 font-mono text-emerald-300">{m.cpuTime}</td>
<td className="text-right px-3 py-2 font-mono text-cyan-300">{m.ioTime}</td>
<td className="text-right px-3 py-2 font-mono">{m.cpuUtilization.toFixed(1)}%</td>
<td className="text-center px-3 py-2">
<Badge variant={boundVariant(m.boundType)}>{m.boundType}</Badge>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default MetricsTable;
+1 -1
View File
@@ -78,7 +78,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
if (i > 0 && bursts[i - 1].type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
});
setErrors(errs);
return errs.length === 0;
@@ -1,15 +1,16 @@
import type { SchedulingAlgorithm } from '@core/types';
import { useProcessStore } from '../../store/processStore';
const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string }[] = [
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: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue (em breve)', disabled: true },
];
const AlgorithmSelector = () => {
@@ -32,7 +33,7 @@ const AlgorithmSelector = () => {
aria-label="Selecionar algoritmo de escalonamento"
>
{ALGORITHM_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
{opt.label}
</option>
))}
@@ -2,13 +2,13 @@ import { useProcessStore } from '../../store/processStore';
import type { SchedulingAlgorithm } from '@core/types';
// Algorithms that require a quantum parameter
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'MULTILEVEL']);
const QUANTUM_ALGORITHMS = new Set<SchedulingAlgorithm>(['RR', 'PRIORITY_RR', 'MULTILEVEL']);
// Algorithms where aging (anti-starvation) is applicable
const AGING_ALGORITHMS = new Set<SchedulingAlgorithm>(['PRIORITY_NP', 'PRIORITY_P']);
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', 'MULTILEVEL']);
const PREEMPTIVE_BY_DESIGN = new Set<SchedulingAlgorithm>(['SJF_P', 'RR', 'PRIORITY_P', 'PRIORITY_RR', 'MULTILEVEL']);
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';
+1 -1
View File
@@ -28,5 +28,5 @@ const PALETTE = [
];
let _paletteIndex = 0;
export function nextColor(): string {
return PALETTE[_paletteIndex++ % PALETTE.length];
return PALETTE[_paletteIndex++ % PALETTE.length]!;
}
+716
View File
@@ -0,0 +1,716 @@
/**
* Integration Tests — Process Scheduler Simulator
*
* These tests validate end-to-end simulation correctness: processes go through
* the full lifecycle (New → Ready → Running → Waiting → Terminated), metrics
* are consistent, and each algorithm produces the expected scheduling behavior.
*
* Test IDs follow the spec (CT-05 onward, CT-01..CT-04 are in engine.test.ts).
*/
import { describe, it, expect } from "vitest";
import { SimulationEngine } from "../engine";
import { computeProcessMetrics, computeGlobalMetrics } from "../metrics";
import { presets } from "../presets";
import type { Process, SchedulerConfig } from "../types";
// ─── Helpers ──────────────────────────────────────────────────────────────────
const cfg = (overrides: Partial<SchedulerConfig> = {}): SchedulerConfig => ({
algorithm: "FCFS",
quantum: 2,
contextSwitchTime: 0,
isPreemptive: false,
agingEnabled: false,
agingInterval: 5,
...overrides,
});
const p = (
id: string,
arrivalTime: number,
bursts: { type: "cpu" | "io"; duration: number }[],
priority = 5,
): Process => ({
id,
name: id.toUpperCase(),
arrivalTime,
priority,
color: "#fff",
bursts,
});
/** Runs the engine and returns per-process metrics keyed by processId */
function runAndGetMetrics(processes: Process[], config: SchedulerConfig) {
const engine = new SimulationEngine(processes, config);
engine.runAll();
const rts = engine.runtimeStates;
const metricsMap = Object.fromEntries(
rts.map((rt) => [rt.processId, computeProcessMetrics(rt)]),
);
const globalMetrics = computeGlobalMetrics(
Object.values(metricsMap),
engine.currentTick + 1,
);
return { engine, metricsMap, globalMetrics };
}
// ─── CT-05: Convoy Effect (FCFS) ──────────────────────────────────────────────
describe("CT-05: FCFS Convoy Effect", () => {
/**
* Large process P1 arrives first and blocks short processes P2, P3, P4.
* P1: arrival=0, CPU:12
* P2: arrival=1, CPU:2
* P3: arrival=2, CPU:1
* P4: arrival=3, CPU:3
*
* Expected Gantt: P1(0-11), P2(12-13), P3(14), P4(15-17)
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 12 }]),
p("p2", 1, [{ type: "cpu", duration: 2 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }]),
p("p4", 3, [{ type: "cpu", duration: 3 }]),
];
it("P1 (long process) runs uninterrupted for 12 ticks", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "FCFS" }));
const ticks = engine.runAll();
for (let i = 0; i <= 11; i++) {
expect(ticks[i]?.cpuProcess).toBe("p1");
}
});
it("short processes execute in arrival order after P1 finishes", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "FCFS" }));
const ticks = engine.runAll();
expect(ticks[12]?.cpuProcess).toBe("p2");
expect(ticks[13]?.cpuProcess).toBe("p2");
expect(ticks[14]?.cpuProcess).toBe("p3");
expect(ticks[15]?.cpuProcess).toBe("p4");
});
it("P2, P3, P4 suffer high waiting times (convoy effect)", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "FCFS" }));
// P2: waited ticks 1-11 = 11 ticks
expect(metricsMap["p2"]!.waitingTime).toBe(11);
// P3: waited ticks 2-13 = 12 ticks
expect(metricsMap["p3"]!.waitingTime).toBe(12);
// P4: waited ticks 3-14 = 12 ticks
expect(metricsMap["p4"]!.waitingTime).toBe(12);
});
it("SJF-NP dramatically reduces average waiting time for the same input", () => {
const { globalMetrics: fcfsGlobal } = runAndGetMetrics(processes, cfg({ algorithm: "FCFS" }));
const { globalMetrics: sjfGlobal } = runAndGetMetrics(processes, cfg({ algorithm: "SJF_NP" }));
// SJF should yield much lower average waiting time than FCFS (convoy effect)
expect(sjfGlobal.avgWaitingTime).toBeLessThan(fcfsGlobal.avgWaitingTime);
});
});
// ─── CT-06: SJF Non-Preemptive ordering ───────────────────────────────────────
describe("CT-06: SJF Non-Preemptive ordering", () => {
/**
* All processes arrive at tick 0. SJF picks shortest first.
* P1: CPU:5, P2: CPU:2, P3: CPU:1, P4: CPU:3
* Expected order: P3(CPU:1) → P2(CPU:2) → P4(CPU:3) → P1(CPU:5)
* Gantt: P3(0-0), P2(1-2), P4(3-5), P1(6-10)
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 5 }]),
p("p2", 0, [{ type: "cpu", duration: 2 }]),
p("p3", 0, [{ type: "cpu", duration: 1 }]),
p("p4", 0, [{ type: "cpu", duration: 3 }]),
];
it("selects processes in ascending burst duration order", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "SJF_NP" }));
const ticks = engine.runAll();
// P3 (CPU:1) first
expect(ticks[0]?.cpuProcess).toBe("p3");
// P2 (CPU:2) second
expect(ticks[1]?.cpuProcess).toBe("p2");
expect(ticks[2]?.cpuProcess).toBe("p2");
// P4 (CPU:3) third
expect(ticks[3]?.cpuProcess).toBe("p4");
expect(ticks[4]?.cpuProcess).toBe("p4");
expect(ticks[5]?.cpuProcess).toBe("p4");
// P1 (CPU:5) last
expect(ticks[6]?.cpuProcess).toBe("p1");
});
it("P3 has zero waiting time (runs first)", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "SJF_NP" }));
expect(metricsMap["p3"]!.waitingTime).toBe(0);
expect(metricsMap["p3"]!.responseTime).toBe(0);
});
it("produces optimal average waiting time (SJF is optimal for non-preemptive)", () => {
const { globalMetrics } = runAndGetMetrics(processes, cfg({ algorithm: "SJF_NP" }));
// Avg waiting: (0 + 1 + 3 + 6) / 4 = 10/4 = 2.5
expect(globalMetrics.avgWaitingTime).toBeCloseTo(2.5, 5);
});
});
// ─── CT-07: Priority NP Starvation ───────────────────────────────────────────
describe("CT-07: Priority NP — P5 starves until all high-priority processes finish", () => {
/**
* P1-P4 have priority 1 (highest). P5 has priority 10 (lowest).
* P5 arrives first but never gets CPU until all P1-P4 finish.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 3 }], 1),
p("p2", 1, [{ type: "cpu", duration: 3 }], 1),
p("p3", 2, [{ type: "cpu", duration: 3 }], 1),
p("p4", 3, [{ type: "cpu", duration: 3 }], 1),
p("p5", 0, [{ type: "cpu", duration: 5 }], 10),
];
it("P5 does not run while any high-priority process is in the ready queue", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "PRIORITY_NP" }));
const ticks = engine.runAll();
// Ticks 0-11: only P1-P4 should be on CPU
for (let i = 0; i <= 11; i++) {
expect(ticks[i]?.cpuProcess).not.toBe("p5");
}
});
it("P5 starts running only after all high-priority processes finish", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "PRIORITY_NP" }));
const ticks = engine.runAll();
// P4 finishes at tick 12 (runs 9-11); P5 starts at 12
expect(ticks[12]?.cpuProcess).toBe("p5");
});
it("P5 has the highest waiting time", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "PRIORITY_NP" }));
const p5Waiting = metricsMap["p5"]!.waitingTime;
// P5 waited from tick 0 until tick 12 → waiting = 12
expect(p5Waiting).toBe(12);
// P5 has more waiting time than all others combined
const others = ["p1", "p2", "p3", "p4"].map((id) => metricsMap[id]!.waitingTime);
expect(p5Waiting).toBeGreaterThan(Math.max(...others));
});
});
// ─── CT-08: Aging prevents starvation ────────────────────────────────────────
describe("CT-08: Priority Preemptive with aging — P5 eventually runs earlier", () => {
/**
* Same scenario as CT-07 but with aging enabled (agingInterval=3).
* P5 should have a lower waiting time than in CT-07 because aging
* eventually promotes it to run before all high-priority processes finish.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 3 }], 1),
p("p2", 1, [{ type: "cpu", duration: 3 }], 1),
p("p3", 2, [{ type: "cpu", duration: 3 }], 1),
p("p4", 3, [{ type: "cpu", duration: 3 }], 1),
p("p5", 0, [{ type: "cpu", duration: 5 }], 10),
];
const agingConfig = cfg({
algorithm: "PRIORITY_P",
isPreemptive: true,
agingEnabled: true,
agingInterval: 3,
});
const noAgingConfig = cfg({ algorithm: "PRIORITY_NP" });
it("all processes eventually terminate with aging enabled", () => {
const engine = new SimulationEngine(processes, agingConfig);
engine.runAll();
expect(engine.isFinished).toBe(true);
const rts = engine.runtimeStates;
for (const rt of rts) {
expect(rt.state).toBe("Terminated");
}
});
it("P5 waiting time with aging is less than or equal to without aging", () => {
const { metricsMap: withAging } = runAndGetMetrics(processes, agingConfig);
const { metricsMap: withoutAging } = runAndGetMetrics(processes, noAgingConfig);
expect(withAging["p5"]!.waitingTime).toBeLessThanOrEqual(withoutAging["p5"]!.waitingTime);
});
});
// ─── CT-09: Multiple I/O Cycles ──────────────────────────────────────────────
describe("CT-09: Process with multiple CPU/I/O cycles", () => {
/**
* P1 alternates: CPU:2 → IO:3 → CPU:1 → IO:2 → CPU:3
* This validates state machine across multiple burst transitions.
*/
const processes = [
p("p1", 0, [
{ type: "cpu", duration: 2 },
{ type: "io", duration: 3 },
{ type: "cpu", duration: 1 },
{ type: "io", duration: 2 },
{ type: "cpu", duration: 3 },
]),
];
it("traverses all states in correct order", () => {
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
const states = ticks.map((t) => t.states["p1"]);
expect(states[0]).toBe("Running"); // CPU:2 ticks 0-1
expect(states[1]).toBe("Running");
expect(states[2]).toBe("Waiting"); // IO:3 ticks 2-4
expect(states[3]).toBe("Waiting");
expect(states[4]).toBe("Waiting");
expect(states[5]).toBe("Running"); // CPU:1 tick 5
expect(states[6]).toBe("Waiting"); // IO:2 ticks 6-7
expect(states[7]).toBe("Waiting");
expect(states[8]).toBe("Running"); // CPU:3 ticks 8-10
expect(states[9]).toBe("Running");
expect(states[10]).toBe("Running");
});
it("finishes at the correct tick", () => {
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rt = engine.runtimeStates[0]!;
// Total duration: 2+3+1+2+3 = 11 ticks → finishTick = 11
expect(rt.finishTick).toBe(11);
});
it("accumulates correct cpu and io times", () => {
const engine = new SimulationEngine(processes, cfg());
engine.runAll();
const rt = engine.runtimeStates[0]!;
expect(rt.cpuTime).toBe(6); // 2+1+3
expect(rt.ioTime).toBe(5); // 3+2
});
it("metrics: turnaround = cpuTime + ioTime + waitingTime", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg());
const m = metricsMap["p1"]!;
// For a single process, waiting time should be 0 (never in Ready queue)
expect(m.waitingTime).toBe(0);
expect(m.turnaroundTime).toBe(m.cpuTime + m.ioTime + m.waitingTime);
});
});
// ─── CT-10: Simultaneous arrivals — tie-breaking ──────────────────────────────
describe("CT-10: Simultaneous arrivals — tie-breaking by ID", () => {
/**
* All 4 processes arrive at tick 0 with equal priority and burst duration.
* FCFS tie-break: by processId lexicographic order → p1, p2, p3, p4.
*/
const processes = [
p("p4", 0, [{ type: "cpu", duration: 2 }]),
p("p2", 0, [{ type: "cpu", duration: 2 }]),
p("p1", 0, [{ type: "cpu", duration: 2 }]),
p("p3", 0, [{ type: "cpu", duration: 2 }]),
];
it("executes processes in lexicographic ID order on tie", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "FCFS" }));
const ticks = engine.runAll();
expect(ticks[0]?.cpuProcess).toBe("p1");
expect(ticks[1]?.cpuProcess).toBe("p1");
expect(ticks[2]?.cpuProcess).toBe("p2");
expect(ticks[3]?.cpuProcess).toBe("p2");
expect(ticks[4]?.cpuProcess).toBe("p3");
expect(ticks[5]?.cpuProcess).toBe("p3");
expect(ticks[6]?.cpuProcess).toBe("p4");
expect(ticks[7]?.cpuProcess).toBe("p4");
});
it("first process (p1) has response time 0 and zero waiting time", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "FCFS" }));
expect(metricsMap["p1"]!.responseTime).toBe(0);
expect(metricsMap["p1"]!.waitingTime).toBe(0);
});
});
// ─── CT-11: CPU idle gap between arrivals ─────────────────────────────────────
describe("CT-11: CPU idle gap between process batches", () => {
/**
* P1 finishes early; CPU becomes idle until P2 arrives later.
* P1: arrival=0, CPU:2
* P2: arrival=6, CPU:2
* Gap: ticks 2-5 (CPU idle for 4 ticks)
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 2 }]),
p("p2", 6, [{ type: "cpu", duration: 2 }]),
];
it("CPU is idle during the gap (no cpuProcess)", () => {
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
// ticks 2,3,4,5: CPU should be idle
for (let i = 2; i <= 5; i++) {
expect(ticks[i]?.cpuProcess).toBeNull();
}
});
it("P2 starts immediately on arrival", () => {
const engine = new SimulationEngine(processes, cfg());
const ticks = engine.runAll();
expect(ticks[6]?.cpuProcess).toBe("p2");
});
it("P2 has zero waiting time (no competition)", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg());
expect(metricsMap["p2"]!.waitingTime).toBe(0);
expect(metricsMap["p2"]!.responseTime).toBe(0);
});
it("global CPU utilization reflects idle period", () => {
const { globalMetrics } = runAndGetMetrics(processes, cfg());
// Total CPU used: 4 ticks out of 8 ticks = 50%
expect(globalMetrics.cpuUtilization).toBeCloseTo(50, 5);
});
});
// ─── CT-12: Round Robin with quantum=1 ───────────────────────────────────────
describe("CT-12: Round Robin quantum=1 — strict rotation", () => {
/**
* P1(CPU:3), P2(CPU:2), P3(CPU:1) all arrive at tick 0, quantum=1.
* Each tick a different process runs in round-robin order.
* Expected: P1(0), P2(1), P3(2), P1(3), P2(4), P1(5)
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 3 }]),
p("p2", 0, [{ type: "cpu", duration: 2 }]),
p("p3", 0, [{ type: "cpu", duration: 1 }]),
];
it("rotates one tick per process", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 1 }));
const ticks = engine.runAll();
expect(ticks[0]?.cpuProcess).toBe("p1");
expect(ticks[1]?.cpuProcess).toBe("p2");
expect(ticks[2]?.cpuProcess).toBe("p3");
expect(ticks[3]?.cpuProcess).toBe("p1");
expect(ticks[4]?.cpuProcess).toBe("p2");
expect(ticks[5]?.cpuProcess).toBe("p1");
});
it("all processes finish at expected ticks", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 1 }));
engine.runAll();
const rts = engine.runtimeStates;
const rt1 = rts.find((r) => r.processId === "p1")!;
const rt2 = rts.find((r) => r.processId === "p2")!;
const rt3 = rts.find((r) => r.processId === "p3")!;
expect(rt3.finishTick).toBe(3); // P3 finishes after 1 run at tick 2
expect(rt2.finishTick).toBe(5); // P2 finishes after 2 runs
expect(rt1.finishTick).toBe(6); // P1 finishes after 3 runs
});
});
// ─── CT-13: Context switch overhead ──────────────────────────────────────────
describe("CT-13: Context switch overhead with multiple algorithms", () => {
/**
* Each process switch incurs 1 idle tick (contextSwitchTime=1).
* Tests that context switch overhead is correctly accounted for
* in multiple algorithm contexts.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 2 }]),
p("p2", 0, [{ type: "cpu", duration: 2 }]),
p("p3", 0, [{ type: "cpu", duration: 2 }]),
];
it("FCFS: inserts 2 context switch ticks for 3 processes", () => {
const engine = new SimulationEngine(
processes,
cfg({ algorithm: "FCFS", contextSwitchTime: 1 }),
);
const ticks = engine.runAll();
// P1: 0-1, ctx: 2, P2: 3-4, ctx: 5, P3: 6-7
expect(ticks[2]?.contextSwitching).toBe(true);
expect(ticks[2]?.cpuProcess).toBeNull();
expect(ticks[5]?.contextSwitching).toBe(true);
expect(ticks[5]?.cpuProcess).toBeNull();
expect(ticks.length).toBe(8);
});
it("total simulation time is longer with context switch", () => {
const withCtx = new SimulationEngine(processes, cfg({ algorithm: "FCFS", contextSwitchTime: 1 }));
const withoutCtx = new SimulationEngine(processes, cfg({ algorithm: "FCFS", contextSwitchTime: 0 }));
withCtx.runAll();
withoutCtx.runAll();
// With 2 context switches of 1 tick each: +2 ticks total
expect(withCtx.currentTick).toBe(withoutCtx.currentTick + 2);
});
it("context switch ticks are not counted as cpu time for any process", () => {
const engine = new SimulationEngine(
processes,
cfg({ algorithm: "FCFS", contextSwitchTime: 1 }),
);
engine.runAll();
const totalCpuTime = engine.runtimeStates.reduce((s, rt) => s + rt.cpuTime, 0);
// 3 processes × 2 cpu ticks each = 6
expect(totalCpuTime).toBe(6);
});
});
// ─── CT-14: HRRN fairness — waited process wins over fresh arrivals ───────────
describe("CT-14: HRRN — long wait compensates for longer burst", () => {
/**
* P1(CPU:1) runs first (only one ready at tick 0).
* When P1 finishes at tick 1, P2(CPU:6, waited=1) and P3(CPU:2, waited=0) are both ready.
*
* HRRN ratios at tick 1:
* P2: (1 + 6) / 6 = 1.167
* P3: (0 + 2) / 2 = 1.0
*
* P2 wins because it has been waiting.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 1 }]),
p("p2", 0, [{ type: "cpu", duration: 6 }]),
p("p3", 1, [{ type: "cpu", duration: 2 }]),
];
it("P2 wins over fresh P3 due to higher response ratio", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "HRRN" }));
const ticks = engine.runAll();
// tick 1: P2 starts (ratio 1.167 > P3 ratio 1.0)
expect(ticks[1]?.cpuProcess).toBe("p2");
});
it("P3 runs only after P2 finishes", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "HRRN" }));
const ticks = engine.runAll();
// P2 runs ticks 1-6; P3 starts at tick 7
expect(ticks[7]?.cpuProcess).toBe("p3");
});
});
// ─── CT-15: SRTF — preemption on arrival with complex I/O ────────────────────
describe("CT-15: SRTF preemption with I/O processes", () => {
/**
* P1(CPU:5,IO:2,CPU:2) starts at 0.
* P2(CPU:2) arrives at tick 2 — preempts P1 (remaining=3 > 2).
* While P2 runs, P1 goes to ready queue.
* P2 finishes at tick 4; P1 resumes its CPU burst.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 5 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
p("p2", 2, [{ type: "cpu", duration: 2 }]),
];
it("P2 preempts P1 at tick 2", () => {
const engine = new SimulationEngine(
processes,
cfg({ algorithm: "SJF_P", isPreemptive: true }),
);
const ticks = engine.runAll();
expect(ticks[0]?.cpuProcess).toBe("p1");
expect(ticks[1]?.cpuProcess).toBe("p1");
expect(ticks[2]?.cpuProcess).toBe("p2"); // preemption here
expect(ticks[3]?.cpuProcess).toBe("p2");
});
it("P1 eventually completes all burst phases and terminates", () => {
const engine = new SimulationEngine(
processes,
cfg({ algorithm: "SJF_P", isPreemptive: true }),
);
engine.runAll();
expect(engine.isFinished).toBe(true);
const p1rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
expect(p1rt.state).toBe("Terminated");
expect(p1rt.ioTime).toBe(2);
});
});
// ─── CT-16: Metrics invariant across all algorithms ──────────────────────────
describe("CT-16: Metrics invariant — turnaround = cpuTime + ioTime + waitingTime", () => {
/**
* For any completed process:
* turnaround = cpuTime + ioTime + waitingTime
*
* This invariant must hold for every algorithm.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
p("p2", 1, [{ type: "cpu", duration: 3 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }, { type: "io", duration: 3 }, { type: "cpu", duration: 2 }]),
];
const algorithms: SchedulerConfig["algorithm"][] = [
"FCFS", "SJF_NP", "SJF_P", "RR", "PRIORITY_NP", "PRIORITY_P", "HRRN",
];
for (const algorithm of algorithms) {
it(`holds for ${algorithm}`, () => {
const isPreemptive = algorithm === "SJF_P" || algorithm === "PRIORITY_P";
const { metricsMap } = runAndGetMetrics(
processes,
cfg({ algorithm, isPreemptive, quantum: 2 }),
);
for (const [id, m] of Object.entries(metricsMap)) {
expect(m.turnaroundTime, `${algorithm}: ${id} turnaround invariant`).toBeCloseTo(
m.cpuTime + m.ioTime + m.waitingTime,
5,
);
}
});
}
});
// ─── CT-17: All processes always terminate ────────────────────────────────────
describe("CT-17: All processes eventually terminate (no infinite loops)", () => {
/**
* For every supported algorithm, a simulation must terminate.
* This validates the engine has no deadlocks or infinite scheduling loops.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 3 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
p("p2", 0, [{ type: "cpu", duration: 5 }]),
p("p3", 2, [{ type: "cpu", duration: 1 }]),
p("p4", 3, [{ type: "cpu", duration: 4 }, { type: "io", duration: 1 }, { type: "cpu", duration: 1 }]),
];
const algorithmsWithConfig: [string, SchedulerConfig][] = [
["FCFS", cfg({ algorithm: "FCFS" })],
["SJF_NP", cfg({ algorithm: "SJF_NP" })],
["SJF_P", cfg({ algorithm: "SJF_P", isPreemptive: true })],
["RR q=2", cfg({ algorithm: "RR", quantum: 2 })],
["RR q=1", cfg({ algorithm: "RR", quantum: 1 })],
["PRIORITY_NP", cfg({ algorithm: "PRIORITY_NP" })],
["PRIORITY_P", cfg({ algorithm: "PRIORITY_P", isPreemptive: true })],
["PRIORITY_P+aging", cfg({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 })],
["HRRN", cfg({ algorithm: "HRRN" })],
];
for (const [name, config] of algorithmsWithConfig) {
it(`${name}: all processes reach Terminated state`, () => {
const engine = new SimulationEngine(processes, config);
engine.runAll();
expect(engine.isFinished).toBe(true);
for (const rt of engine.runtimeStates) {
expect(rt.state, `${name}: process ${rt.processId} should be Terminated`).toBe("Terminated");
}
});
}
});
// ─── CT-18: Preset validation ─────────────────────────────────────────────────
describe("CT-18: All presets complete successfully", () => {
/**
* Each preset must run to completion with all processes terminated,
* and produce valid (non-negative) metrics.
*/
for (const preset of presets) {
it(`preset "${preset.name}" terminates and yields valid metrics`, () => {
const engine = new SimulationEngine(preset.processes, preset.config);
engine.runAll();
expect(engine.isFinished).toBe(true);
for (const rt of engine.runtimeStates) {
expect(rt.state).toBe("Terminated");
expect(rt.finishTick).not.toBeNull();
expect(rt.startTick).not.toBeNull();
const m = computeProcessMetrics(rt);
expect(m.turnaroundTime).toBeGreaterThan(0);
expect(m.waitingTime).toBeGreaterThanOrEqual(0);
expect(m.responseTime).toBeGreaterThanOrEqual(0);
expect(m.cpuTime).toBeGreaterThan(0);
expect(m.ioTime).toBeGreaterThanOrEqual(0);
// Invariant: turnaround = cpuTime + ioTime + waitingTime
expect(m.turnaroundTime).toBeCloseTo(m.cpuTime + m.ioTime + m.waitingTime, 5);
}
});
}
});
// ─── CT-19: RR with I/O — quantum resets correctly after I/O return ───────────
describe("CT-19: RR with I/O — process returning from I/O gets fresh quantum", () => {
/**
* P1(CPU:4, IO:2, CPU:2) with quantum=2.
* P2(CPU:6) with quantum=2.
*
* P1 uses 2 CPU ticks (quantum exhausted) → goes to ready.
* P2 uses 2 ticks → goes to ready.
* P1 uses last 2 CPU ticks → goes to I/O.
* While P1 does I/O, P2 runs.
* P1 returns from I/O and gets a fresh quantum for its final 2 CPU ticks.
*/
const processes = [
p("p1", 0, [{ type: "cpu", duration: 4 }, { type: "io", duration: 2 }, { type: "cpu", duration: 2 }]),
p("p2", 0, [{ type: "cpu", duration: 6 }]),
];
it("P1 and P2 both terminate", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 }));
engine.runAll();
expect(engine.isFinished).toBe(true);
for (const rt of engine.runtimeStates) {
expect(rt.state).toBe("Terminated");
}
});
it("P1 accumulates correct cpu and io times", () => {
const engine = new SimulationEngine(processes, cfg({ algorithm: "RR", quantum: 2 }));
engine.runAll();
const p1rt = engine.runtimeStates.find((r) => r.processId === "p1")!;
expect(p1rt.cpuTime).toBe(6); // 4 + 2
expect(p1rt.ioTime).toBe(2);
});
it("metrics invariant holds for both processes", () => {
const { metricsMap } = runAndGetMetrics(processes, cfg({ algorithm: "RR", quantum: 2 }));
for (const m of Object.values(metricsMap)) {
expect(m.turnaroundTime).toBeCloseTo(m.cpuTime + m.ioTime + m.waitingTime, 5);
}
});
});
// ─── CT-20: Single process — no competition edge case ────────────────────────
describe("CT-20: Single process — baseline edge case", () => {
/**
* With only one process, there is no scheduling decision to make.
* The process should run uninterrupted and have zero waiting time,
* regardless of algorithm.
*/
const process = p("p1", 0, [
{ type: "cpu", duration: 3 },
{ type: "io", duration: 2 },
{ type: "cpu", duration: 2 },
]);
const algorithms: SchedulerConfig["algorithm"][] = ["FCFS", "SJF_NP", "RR", "PRIORITY_NP", "HRRN"];
for (const algorithm of algorithms) {
it(`${algorithm}: single process has zero waiting time`, () => {
const { metricsMap } = runAndGetMetrics([process], cfg({ algorithm, quantum: 2 }));
expect(metricsMap["p1"]!.waitingTime).toBe(0);
expect(metricsMap["p1"]!.responseTime).toBe(0);
});
}
});
+248
View File
@@ -0,0 +1,248 @@
import { describe, it, expect } from "vitest";
import { priorityRr } from "../algorithms/priority_rr";
import { SimulationEngine } from "../engine";
import type { Process, ProcessRuntime, SchedulerConfig } from "../types";
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
function makeRt(
id: string,
priority: number,
quantumRemaining = 2,
): ProcessRuntime {
return {
processId: id,
state: "Ready",
remainingBurst: 4,
burstIndex: 0,
arrivalTick: 0,
startTick: null,
finishTick: null,
waitingTime: 0,
cpuTime: 0,
ioTime: 0,
responseTime: null,
quantumRemaining,
currentPriority: priority,
};
}
const cfg = (quantum = 2): SchedulerConfig => ({
algorithm: "PRIORITY_RR",
quantum,
contextSwitchTime: 0,
isPreemptive: true,
agingEnabled: false,
agingInterval: 5,
});
const p = (
id: string,
arrival: number,
cpuDuration: number,
priority = 5,
): Process => ({
id,
name: id,
arrivalTime: arrival,
priority,
color: "#fff",
bursts: [{ type: "cpu", duration: cpuDuration }],
});
// ----------------------------------------------------------------
// Unit: select()
// ----------------------------------------------------------------
describe("PRIORITY_RR select()", () => {
it("returns null for empty queue", () => {
expect(priorityRr.select([], cfg())).toBeNull();
});
it("picks the process with the lowest priority number (highest priority)", () => {
const high = makeRt("high", 1);
const low = makeRt("low", 5);
expect(priorityRr.select([low, high], cfg())?.processId).toBe("high");
expect(priorityRr.select([high, low], cfg())?.processId).toBe("high");
});
it("picks the FIRST in insertion order when priorities are equal (FIFO = RR)", () => {
const a = makeRt("a", 3);
const b = makeRt("b", 3);
const c = makeRt("c", 3);
expect(priorityRr.select([a, b, c], cfg())?.processId).toBe("a");
expect(priorityRr.select([b, c, a], cfg())?.processId).toBe("b");
});
it("ignores lower-priority processes when a higher-priority one is present", () => {
const critical = makeRt("critical", 1);
const normal1 = makeRt("n1", 5);
const normal2 = makeRt("n2", 5);
expect(
priorityRr.select([normal1, normal2, critical], cfg())?.processId,
).toBe("critical");
});
});
// ----------------------------------------------------------------
// Unit: shouldPreempt()
// ----------------------------------------------------------------
describe("PRIORITY_RR shouldPreempt()", () => {
it("preempts when candidate has strictly higher priority", () => {
const running = makeRt("current", 5);
const higher = makeRt("higher", 1);
expect(priorityRr.shouldPreempt(running, higher, cfg())).toBe(true);
});
it("does NOT preempt for same priority (quantum handles rotation)", () => {
const a = makeRt("a", 3);
const b = makeRt("b", 3);
expect(priorityRr.shouldPreempt(a, b, cfg())).toBe(false);
});
it("does NOT preempt when candidate has lower priority", () => {
const running = makeRt("current", 1);
const lower = makeRt("lower", 5);
expect(priorityRr.shouldPreempt(running, lower, cfg())).toBe(false);
});
});
// ----------------------------------------------------------------
// Unit: isQuantumExpired()
// ----------------------------------------------------------------
describe("PRIORITY_RR isQuantumExpired()", () => {
it("returns true when quantumRemaining <= 0", () => {
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 0), cfg())).toBe(true);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, -1), cfg())).toBe(true);
});
it("returns false when quantum is not exhausted", () => {
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 1), cfg())).toBe(false);
expect(priorityRr.isQuantumExpired(makeRt("p", 5, 2), cfg())).toBe(false);
});
});
// ----------------------------------------------------------------
// Integration: Round-robin within the same priority band
// ----------------------------------------------------------------
describe("PRIORITY_RR integration — RR within same priority", () => {
it("two equal-priority processes rotate via quantum (CT-PRIORITY_RR-01)", () => {
// P1 and P2 both priority=2, CPU:4 each, quantum=2
// Expected Gantt: P1(0-2), P2(2-4), P1(4-6), P2(6-8)
const processes = [p("p1", 0, 4, 2), p("p2", 0, 4, 2)];
const engine = new SimulationEngine(processes, cfg(2));
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu).toHaveLength(8);
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("three equal-priority processes rotate in arrival order", () => {
// P1(CPU:2), P2(CPU:2), P3(CPU:2), quantum=2 — each finishes in first quantum
const processes = [p("p1", 0, 2, 3), p("p2", 0, 2, 3), p("p3", 0, 2, 3)];
const engine = new SimulationEngine(processes, cfg(2));
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu).toHaveLength(6);
expect(cpu[0]).toBe("p1");
expect(cpu[1]).toBe("p1");
expect(cpu[2]).toBe("p2");
expect(cpu[3]).toBe("p2");
expect(cpu[4]).toBe("p3");
expect(cpu[5]).toBe("p3");
});
});
// ----------------------------------------------------------------
// Integration: Priority preemption
// ----------------------------------------------------------------
describe("PRIORITY_RR integration — priority preemption", () => {
it("high-priority process arriving mid-run immediately preempts (CT-PRIORITY_RR-02)", () => {
// P1: priority=3, arrival=0, CPU:6 (large quantum so it won't expire)
// P2: priority=1, arrival=2, CPU:2
// Expected: P1(0-1), P2 preempts at tick 2, P2(2-3), P1 resumes(4-7)
const processes = [p("p1", 0, 6, 3), p("p2", 2, 2, 1)];
const engine = new SimulationEngine(processes, cfg(10));
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
// P1 runs ticks 0, 1; P2 preempts at tick 2; P1 resumes after P2 finishes
expect(cpu[0]).toBe("p1");
expect(cpu[1]).toBe("p1");
expect(cpu[2]).toBe("p2"); // preempted here
expect(cpu[3]).toBe("p2");
// After P2 terminates, P1 resumes
expect(cpu[4]).toBe("p1");
expect(cpu[5]).toBe("p1");
expect(cpu[6]).toBe("p1");
expect(cpu[7]).toBe("p1");
expect(cpu).toHaveLength(8);
});
it("lower-priority process cannot preempt a higher-priority running process", () => {
const processes = [p("p1", 0, 4, 1), p("p2", 1, 4, 5)];
const engine = new SimulationEngine(processes, cfg(10));
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
// P1 (priority=1) runs entirely first; P2 waits
expect(cpu.slice(0, 4).every((id) => id === "p1")).toBe(true);
expect(cpu.slice(4).every((id) => id === "p2")).toBe(true);
});
});
// ----------------------------------------------------------------
// Integration: Combined — preemption + RR (preset scenario)
// ----------------------------------------------------------------
describe("PRIORITY_RR integration — combined preemption + RR", () => {
it("preset scenario: P1 & P2 (prio=2) RR, P3 (prio=1) preempts at arrival", () => {
// P1: prio=2, arrival=0, CPU:4
// P2: prio=2, arrival=0, CPU:4
// P3: prio=1, arrival=4, CPU:2 — arrives just as P2's first quantum ends
// quantum=2
// Expected Gantt: P1(0-2), P2(2-4), P3(4-6), P1(6-8), P2(8-10)
const processes = [p("p1", 0, 4, 2), p("p2", 0, 4, 2), p("p3", 4, 2, 1)];
const engine = new SimulationEngine(processes, cfg(2));
const ticks = engine.runAll();
const cpu = ticks.map((t) => t.cpuProcess);
expect(cpu[0]).toBe("p1");
expect(cpu[1]).toBe("p1");
expect(cpu[2]).toBe("p2");
expect(cpu[3]).toBe("p2");
expect(cpu[4]).toBe("p3"); // P3 preempts (or takes CPU when it's free)
expect(cpu[5]).toBe("p3");
expect(cpu[6]).toBe("p1");
expect(cpu[7]).toBe("p1");
expect(cpu[8]).toBe("p2");
expect(cpu[9]).toBe("p2");
expect(cpu).toHaveLength(10);
});
it("all processes terminate correctly", () => {
const processes = [p("p1", 0, 4, 2), p("p2", 0, 4, 2), p("p3", 4, 2, 1)];
const engine = new SimulationEngine(processes, cfg(2));
engine.runAll();
const rts = engine.runtimeStates;
expect(rts.every((rt) => rt.state === "Terminated")).toBe(true);
expect(rts.find((r) => r.processId === "p1")!.cpuTime).toBe(4);
expect(rts.find((r) => r.processId === "p2")!.cpuTime).toBe(4);
expect(rts.find((r) => r.processId === "p3")!.cpuTime).toBe(2);
});
});
+2
View File
@@ -3,6 +3,7 @@ import { fcfs } from "./fcfs";
import { sjfNp, srtf } from "./sjf";
import { rr } from "./rr";
import { priorityNp, priorityP } from "./priority";
import { priorityRr } from "./priority_rr";
import { hrrn } from "./hrrn";
import { multilevel } from "./multilevel";
@@ -54,6 +55,7 @@ export const algorithms: Record<SchedulingAlgorithm, SchedulerAlgorithm> = {
RR: rr,
PRIORITY_NP: priorityNp,
PRIORITY_P: priorityP,
PRIORITY_RR: priorityRr,
HRRN: hrrn,
MULTILEVEL: multilevel,
};
+49
View File
@@ -0,0 +1,49 @@
import type { SchedulerAlgorithm } from "./index";
/**
* Priority Round Robin (PRIORITY_RR)
*
* Mimics real-OS scheduling (Windows priority bands, POSIX SCHED_RR):
*
* - The ready queue is conceptually divided into priority bands.
* - The scheduler always dispatches from the highest-priority band
* (lowest numeric value), leaving lower-priority processes waiting.
* - Within the same priority band, processes share the CPU via round-robin
* with the configured quantum — fairness among equals.
* - A running process is preempted immediately when a STRICTLY higher-priority
* process becomes ready (arrival or I/O completion).
* - Quantum expiry only rotates within the same priority band; it does not
* allow a lower-priority process to run while a same-priority one is waiting.
* - Compatible with the aging mechanism: as low-priority processes age their
* currentPriority decreases, eventually entering a higher-priority band.
*/
export const priorityRr: SchedulerAlgorithm = {
select(readyQueue) {
if (readyQueue.length === 0) return null;
// Find the highest priority level (smallest numeric value) present.
let bestPriority = Infinity;
for (const rt of readyQueue) {
if (rt.currentPriority < bestPriority) bestPriority = rt.currentPriority;
}
// Within that band, return the first process (FIFO insertion order).
// The engine maintains insertion order, so this gives round-robin rotation
// after quantum expiry: the expired process goes to the tail, the next
// same-priority process is now at the head.
return readyQueue.find(rt => rt.currentPriority === bestPriority) ?? null;
},
// Preempt ONLY for strictly higher priority.
// Same-priority candidates are handled by quantum expiry — no unnecessary
// context switches within a priority band.
shouldPreempt(current, candidate) {
return candidate.currentPriority < current.currentPriority;
},
// Quantum expiry drives round-robin rotation within a priority band,
// exactly like plain Round Robin.
isQuantumExpired(runtime) {
return runtime.quantumRemaining <= 0;
},
};
+33
View File
@@ -67,6 +67,16 @@ export const presets: Preset[] = [
],
config: defaultConfig({ algorithm: "PRIORITY_P", isPreemptive: true, agingEnabled: true, agingInterval: 3 }),
},
{
name: "priority_rr_demo",
description: "Priority RR: P3 (priority 1) preempts mid-run; P1 & P2 (priority 2) share CPU via RR",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 4 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 2, color: "#81C784", bursts: [{ type: "cpu", duration: 4 }] },
{ id: "p3", name: "P3", arrivalTime: 4, priority: 1, color: "#F06292", bursts: [{ type: "cpu", duration: 2 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
},
{
name: "io_heavy",
description: "Mixed CPU-bound and I/O-bound processes",
@@ -77,4 +87,27 @@ export const presets: Preset[] = [
],
config: defaultConfig({ algorithm: "SJF_P", isPreemptive: true }),
},
{
name: "convoy_effect",
description: "One long process blocks shorter ones — FCFS convoy vs SJF comparison",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#EF5350", bursts: [{ type: "cpu", duration: 12 }] },
{ id: "p2", name: "P2", arrivalTime: 1, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 2 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 5, color: "#FFB74D", bursts: [{ type: "cpu", duration: 1 }] },
{ id: "p4", name: "P4", arrivalTime: 3, priority: 5, color: "#CE93D8", bursts: [{ type: "cpu", duration: 3 }] },
],
config: defaultConfig({ algorithm: "FCFS" }),
},
{
name: "multilevel_demo",
description: "Priority RR with 3 priority tiers — demonstrates multilevel scheduling",
processes: [
{ id: "p1", name: "P1", arrivalTime: 0, priority: 1, color: "#EF5350", bursts: [{ type: "cpu", duration: 4 }] },
{ id: "p2", name: "P2", arrivalTime: 0, priority: 1, color: "#FF8A65", bursts: [{ type: "cpu", duration: 3 }] },
{ id: "p3", name: "P3", arrivalTime: 2, priority: 2, color: "#81C784", bursts: [{ type: "cpu", duration: 5 }] },
{ id: "p4", name: "P4", arrivalTime: 2, priority: 2, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 2 }] },
{ id: "p5", name: "P5", arrivalTime: 4, priority: 3, color: "#CE93D8", bursts: [{ type: "cpu", duration: 6 }] },
],
config: defaultConfig({ algorithm: "PRIORITY_RR", quantum: 2 }),
},
];
+1
View File
@@ -9,6 +9,7 @@ export type SchedulingAlgorithm =
| "RR"
| "PRIORITY_NP"
| "PRIORITY_P"
| "PRIORITY_RR"
| "MULTILEVEL"
| "HRRN";
+56
View File
@@ -0,0 +1,56 @@
import type { ProcessMetrics } from "@core/types";
import type { Process } from "@core/types";
interface ProcessMap {
[id: string]: Process;
}
function downloadBlob(content: string, filename: string, mimeType: string) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
export function useExport() {
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
const headers = [
"Processo",
"Arrival",
"Start",
"Finish",
"Response",
"Turnaround",
"Waiting",
"CPU Time",
"I/O Time",
"CPU%",
"Bound",
];
const rows = metrics.map((m) => {
const name = processMap[m.processId]?.name ?? m.processId;
return [
name,
m.arrivalTime,
m.startTick,
m.finishTick,
m.responseTime,
m.turnaroundTime,
m.waitingTime,
m.cpuTime,
m.ioTime,
m.cpuUtilization.toFixed(1),
m.boundType,
].join(",");
});
const csv = [headers.join(","), ...rows].join("\n");
downloadBlob(csv, "pss-metrics.csv", "text/csv;charset=utf-8;");
};
return { exportCSV };
}
+22
View File
@@ -0,0 +1,22 @@
import { useEffect, type RefObject } from "react";
import { CELL_W } from "../components/GanttChart/constants";
/**
* Smoothly scrolls `containerRef` so the current tick stays horizontally
* centred in the viewport. Only triggers when the simulation is actively
* running (to avoid stealing the user's manual scroll while paused).
*/
export function useGanttScroll(
containerRef: RefObject<HTMLDivElement | null>,
currentTickIndex: number,
isRunning: boolean,
) {
useEffect(() => {
if (!isRunning) return;
const el = containerRef.current;
if (!el) return;
const targetLeft = currentTickIndex * CELL_W - el.clientWidth / 2 + CELL_W / 2;
el.scrollTo({ left: Math.max(0, targetLeft), behavior: "smooth" });
}, [containerRef, currentTickIndex, isRunning]);
}
+24
View File
@@ -17,4 +17,28 @@
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Custom scrollbar for Gantt */
.gantt-scrollbar::-webkit-scrollbar {
height: 6px;
}
.gantt-scrollbar::-webkit-scrollbar-track {
background: #0f172a; /* slate-900 */
}
.gantt-scrollbar::-webkit-scrollbar-thumb {
background: #334155; /* slate-700 */
border-radius: 10px;
}
.gantt-scrollbar::-webkit-scrollbar-thumb:hover {
background: #475569; /* slate-600 */
}
@keyframes gantt-cursor-pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.gantt-cursor {
animation: gantt-cursor-pulse 2s ease-in-out infinite;
}
}
+3 -1
View File
@@ -64,6 +64,8 @@ export const useSimulationStore = create<SimulationStore>()((set, get) => ({
const { _processes, _config } = get();
if (_processes.length === 0 || _config === null) return;
const engine = new SimulationEngine(_processes, _config);
set({ engine, ticks: [], status: "paused" });
// Return to "idle" so the next Play/Step re-runs init() with the
// current processStore config (picks up algorithm changes, etc.)
set({ engine, ticks: [], status: "idle" });
},
}));
+2 -1
View File
@@ -19,7 +19,8 @@
"paths": {
"@core/*": ["./src/core/*"],
"@components/*": ["./src/components/*"],
"@store/*": ["./src/store/*"]
"@store/*": ["./src/store/*"],
"@hooks/*": ["./src/hooks/*"]
}
},
"include": ["src"]
+1
View File
@@ -20,6 +20,7 @@ export default defineConfig({
"@core": path.resolve(__dirname, "./src/core"),
"@components": path.resolve(__dirname, "./src/components"),
"@store": path.resolve(__dirname, "./src/store"),
"@hooks": path.resolve(__dirname, "./src/hooks"),
},
},
});