summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2020-10-30 00:17:01 +0100
committerGitHub <noreply@github.com>2020-10-30 00:17:01 +0100
commit91b38f216f3107d4be2fa26e78c3e6df674bcbca (patch)
tree1f2239bb3758ac3099542fe7b06d559d58bdecd5
parentea0dd07e8a5deb8084ebcbae780e57fdd90bccc2 (diff)
parent4ec2ace2e1ca37294f6e55c2965f1fc6f98d622c (diff)
Merge pull request #56 from atlarge-research/bug/scenario
Fix workings of scenarios
-rw-r--r--api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py6
-rw-r--r--docker-compose.yml5
-rw-r--r--frontend/package.json2
-rw-r--r--frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js8
-rw-r--r--frontend/src/components/modals/Modal.js4
-rw-r--r--frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js145
-rw-r--r--frontend/src/components/modals/custom-components/NewScenarioModalComponent.js268
-rw-r--r--frontend/src/components/modals/custom-components/NewTopologyModalComponent.js121
-rw-r--r--frontend/src/components/navigation/AppNavbarComponent.js9
-rw-r--r--frontend/src/components/navigation/HomeNavbar.js9
-rw-r--r--frontend/src/components/navigation/LogoutButton.js5
-rw-r--r--frontend/src/components/navigation/Navbar.js131
-rw-r--r--frontend/src/containers/modals/NewScenarioModal.js1
-rw-r--r--frontend/src/sagas/objects.js22
-rw-r--r--frontend/yarn.lock40
-rw-r--r--simulator/.dockerignore2
-rw-r--r--simulator/Dockerfile20
-rw-r--r--simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts4
-rw-r--r--simulator/gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--simulator/opendc-runner-web/build.gradle.kts7
-rw-r--r--simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt59
-rw-r--r--simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ResultProcessor.kt215
-rw-r--r--simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ScenarioManager.kt40
-rw-r--r--simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/TopologyParser.kt9
-rw-r--r--simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/WebExperimentMonitor.kt273
-rw-r--r--simulator/opendc-runner-web/src/main/resources/log4j2.xml6
26 files changed, 680 insertions, 733 deletions
diff --git a/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py
index fc2cab16..2f042e06 100644
--- a/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py
+++ b/api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py
@@ -1,5 +1,6 @@
from opendc.models.portfolio import Portfolio
from opendc.models.scenario import Scenario
+from opendc.models.topology import Topology
from opendc.util.rest import Response
@@ -32,8 +33,13 @@ def POST(request):
scenario = Scenario(request.params_body['scenario'])
+ topology = Topology.from_id(scenario.obj['topology']['topologyId'])
+ topology.check_exists()
+ topology.check_user_access(request.google_id, True)
+
scenario.set_property('portfolioId', portfolio.get_id())
scenario.set_property('simulation', {'state': 'QUEUED'})
+ scenario.set_property('topology.topologyId', topology.get_id())
scenario.insert()
diff --git a/docker-compose.yml b/docker-compose.yml
index 4d62b7cd..d49ac5dc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -41,15 +41,11 @@ services:
- type: bind
source: ./traces
target: /app/traces
- - type: volume
- source: results-volume
- target: /results
environment:
- OPENDC_DB
- OPENDC_DB_USERNAME
- OPENDC_DB_PASSWORD
- OPENDC_DB_HOST=mongo
- - OPENDC_OUTPUT=/results
mongo:
build:
@@ -69,7 +65,6 @@ services:
volumes:
mongo-volume:
- results-volume:
networks:
backend: {}
diff --git a/frontend/package.json b/frontend/package.json
index bb190954..16732995 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,7 +16,7 @@
"author": "Georgios Andreadis <g.andreadis@atlarge-research.com> (https://gandreadis.com/)",
"license": "MIT",
"private": true,
- "proxy": "http://localhost:8082",
+ "proxy": "http://localhost:8081",
"dependencies": {
"approximate-number": "~2.0.0",
"bootstrap": "4.5.3",
diff --git a/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js
index 569166d8..6599fefd 100644
--- a/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js
+++ b/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js
@@ -14,7 +14,7 @@ const UnitTabsComponent = () => {
<Nav tabs>
<NavItem>
<NavLink
- className={activeTab === 'cpu-units' && 'active'}
+ className={activeTab === 'cpu-units' ? 'active' : ''}
onClick={() => {
toggle('cpu-units')
}}
@@ -24,7 +24,7 @@ const UnitTabsComponent = () => {
</NavItem>
<NavItem>
<NavLink
- className={activeTab === 'gpu-units' && 'active'}
+ className={activeTab === 'gpu-units' ? 'active' : ''}
onClick={() => {
toggle('gpu-units')
}}
@@ -34,7 +34,7 @@ const UnitTabsComponent = () => {
</NavItem>
<NavItem>
<NavLink
- className={activeTab === 'memory-units' && 'active'}
+ className={activeTab === 'memory-units' ? 'active' : ''}
onClick={() => {
toggle('memory-units')
}}
@@ -44,7 +44,7 @@ const UnitTabsComponent = () => {
</NavItem>
<NavItem>
<NavLink
- className={activeTab === 'storage-units' && 'active'}
+ className={activeTab === 'storage-units' ? 'active' : ''}
onClick={() => {
toggle('storage-units')
}}
diff --git a/frontend/src/components/modals/Modal.js b/frontend/src/components/modals/Modal.js
index 36bb498e..b494d970 100644
--- a/frontend/src/components/modals/Modal.js
+++ b/frontend/src/components/modals/Modal.js
@@ -18,8 +18,8 @@ function Modal({ children, title, show, onSubmit, onCancel, submitButtonType, su
}
return (
- <RModal isOpen={modal} toggle={toggle}>
- <ModalHeader toggle={toggle}>{title}</ModalHeader>
+ <RModal isOpen={modal} toggle={cancel}>
+ <ModalHeader toggle={cancel}>{title}</ModalHeader>
<ModalBody>{children}</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={cancel}>
diff --git a/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js b/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js
index 19049931..978ca11d 100644
--- a/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js
+++ b/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js
@@ -1,98 +1,69 @@
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useRef } from 'react'
+import { Form, FormGroup, Input, Label } from 'reactstrap'
import Modal from '../Modal'
import { AVAILABLE_METRICS } from '../../../util/available-metrics'
-class NewPortfolioModalComponent extends React.Component {
- static propTypes = {
- show: PropTypes.bool.isRequired,
- callback: PropTypes.func.isRequired,
- }
+const NewPortfolioModalComponent = ({ show, callback }) => {
+ const textInput = useRef(null)
+ const repeatsInput = useRef(null)
+ const metricCheckboxes = useRef({})
- constructor(props) {
- super(props)
- this.metricCheckboxes = {}
- }
-
- componentDidMount() {
- this.reset()
- }
-
- reset() {
- if (this.textInput) {
- this.textInput.value = ''
- AVAILABLE_METRICS.forEach((metric) => {
- this.metricCheckboxes[metric].checked = true
- })
- this.repeatsInput.value = 1
- }
- }
-
- onSubmit() {
- this.props.callback(this.textInput.value, {
- enabledMetrics: AVAILABLE_METRICS.filter((metric) => this.metricCheckboxes[metric].checked),
- repeatsPerScenario: parseInt(this.repeatsInput.value),
+ const onSubmit = () =>
+ callback(textInput.current.value, {
+ enabledMetrics: AVAILABLE_METRICS.filter((metric) => metricCheckboxes.current[metric].checked),
+ repeatsPerScenario: parseInt(repeatsInput.current.value),
})
- this.reset()
- }
+ const onCancel = () => callback(undefined)
- onCancel() {
- this.props.callback(undefined)
- this.reset()
- }
-
- render() {
- return (
- <Modal
- title="New Portfolio"
- show={this.props.show}
- onSubmit={this.onSubmit.bind(this)}
- onCancel={this.onCancel.bind(this)}
+ return (
+ <Modal title="New Portfolio" show={show} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault()
+ this.onSubmit()
+ }}
>
- <form
- onSubmit={(e) => {
- e.preventDefault()
- this.onSubmit()
- }}
- >
- <div className="form-group">
- <label className="form-control-label">Name</label>
- <input
- type="text"
- className="form-control"
- required
- ref={(textInput) => (this.textInput = textInput)}
- />
- </div>
- <h4>Targets</h4>
- <h5>Metrics</h5>
- <div className="form-group">
- {AVAILABLE_METRICS.map((metric) => (
- <div className="form-check" key={metric}>
- <label className="form-check-label">
- <input
- type="checkbox"
- className="form-check-input"
- ref={(checkbox) => (this.metricCheckboxes[metric] = checkbox)}
- />
- <code>{metric}</code>
- </label>
- </div>
- ))}
- </div>
- <div className="form-group">
- <label className="form-control-label">Repeats per scenario</label>
- <input
- type="number"
- className="form-control"
- required
- ref={(repeatsInput) => (this.repeatsInput = repeatsInput)}
- />
- </div>
- </form>
- </Modal>
- )
- }
+ <FormGroup>
+ <Label for="name">Name</Label>
+ <Input name="name" type="text" required innerRef={textInput} placeholder="My Portfolio" />
+ </FormGroup>
+ <h4>Targets</h4>
+ <h5>Metrics</h5>
+ <FormGroup>
+ {AVAILABLE_METRICS.map((metric) => (
+ <FormGroup check key={metric}>
+ <Label for={metric} check>
+ <Input
+ name={metric}
+ type="checkbox"
+ innerRef={(ref) => (metricCheckboxes.current[metric] = ref)}
+ />
+ <code>{metric}</code>
+ </Label>
+ </FormGroup>
+ ))}
+ </FormGroup>
+ <FormGroup>
+ <Label for="repeats">Repeats per scenario</Label>
+ <Input
+ name="repeats"
+ type="number"
+ required
+ innerRef={repeatsInput}
+ defaultValue="1"
+ min="1"
+ step="1"
+ />
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewPortfolioModalComponent.propTypes = {
+ show: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
}
export default NewPortfolioModalComponent
diff --git a/frontend/src/components/modals/custom-components/NewScenarioModalComponent.js b/frontend/src/components/modals/custom-components/NewScenarioModalComponent.js
index 5ba74b0f..631082a2 100644
--- a/frontend/src/components/modals/custom-components/NewScenarioModalComponent.js
+++ b/frontend/src/components/modals/custom-components/NewScenarioModalComponent.js
@@ -1,168 +1,138 @@
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useRef } from 'react'
+import { Form, FormGroup, Input, Label } from 'reactstrap'
import Shapes from '../../../shapes'
import Modal from '../Modal'
-class NewScenarioModalComponent extends React.Component {
- static propTypes = {
- show: PropTypes.bool.isRequired,
- currentPortfolioId: PropTypes.string.isRequired,
- currentPortfolioScenarioIds: PropTypes.arrayOf(PropTypes.string),
- traces: PropTypes.arrayOf(Shapes.Trace),
- topologies: PropTypes.arrayOf(Shapes.Topology),
- schedulers: PropTypes.arrayOf(Shapes.Scheduler),
- callback: PropTypes.func.isRequired,
- }
-
- componentDidMount() {
- this.reset()
- }
+const NewScenarioModalComponent = ({
+ show,
+ callback,
+ currentPortfolioId,
+ currentPortfolioScenarioIds,
+ traces,
+ topologies,
+ schedulers,
+}) => {
+ const textInput = useRef(null)
+ const traceSelect = useRef(null)
+ const traceLoadInput = useRef(null)
+ const topologySelect = useRef(null)
+ const failuresCheckbox = useRef(null)
+ const performanceInterferenceCheckbox = useRef(null)
+ const schedulerSelect = useRef(null)
- componentDidUpdate() {
- if (this.textInput) {
- if (this.props.currentPortfolioScenarioIds.length === 0) {
- this.textInput.value = 'Base scenario'
- } else if (this.textInput.value === 'Base scenario') {
- this.textInput.value = ''
- }
- }
- }
-
- reset() {
- if (this.textInput) {
- this.textInput.value = this.props.currentPortfolioScenarioIds.length === 0 ? 'Base scenario' : ''
- this.traceSelect.selectedIndex = 0
- this.traceLoadInput.value = 1.0
- this.topologySelect.selectedIndex = 0
- this.failuresCheckbox.checked = false
- this.performanceInterferenceCheckbox.checked = false
- this.schedulerSelect.selectedIndex = 0
- }
- }
-
- onSubmit() {
- this.props.callback(
- this.textInput.value,
- this.props.currentPortfolioId,
+ const onSubmit = () => {
+ callback(
+ textInput.current.value,
+ currentPortfolioId,
{
- traceId: this.traceSelect.value,
- loadSamplingFraction: parseFloat(this.traceLoadInput.value),
+ traceId: traceSelect.current.value,
+ loadSamplingFraction: parseFloat(traceLoadInput.current.value),
},
{
- topologyId: this.topologySelect.value,
+ topologyId: topologySelect.current.value,
},
{
- failuresEnabled: this.failuresCheckbox.checked,
- performanceInterferenceEnabled: this.performanceInterferenceCheckbox.checked,
- schedulerName: this.schedulerSelect.value,
+ failuresEnabled: failuresCheckbox.current.checked,
+ performanceInterferenceEnabled: performanceInterferenceCheckbox.current.checked,
+ schedulerName: schedulerSelect.current.value,
}
)
- this.reset()
}
-
- onCancel() {
- this.props.callback(undefined)
- this.reset()
+ const onCancel = () => {
+ callback(undefined)
}
- render() {
- return (
- <Modal
- title="New Scenario"
- show={this.props.show}
- onSubmit={this.onSubmit.bind(this)}
- onCancel={this.onCancel.bind(this)}
+ return (
+ <Modal title="New Scenario" show={show} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault()
+ onSubmit()
+ }}
>
- <form
- onSubmit={(e) => {
- e.preventDefault()
- this.onSubmit()
- }}
- >
- <div className="form-group">
- <label className="form-control-label">Name</label>
- <input
- type="text"
- className="form-control"
- required
- disabled={this.props.currentPortfolioScenarioIds.length === 0}
- ref={(textInput) => (this.textInput = textInput)}
- />
- </div>
- <h4>Trace</h4>
- <div className="form-group">
- <label className="form-control-label">Trace</label>
- <select className="form-control" ref={(traceSelect) => (this.traceSelect = traceSelect)}>
- {this.props.traces.map((trace) => (
- <option value={trace._id} key={trace._id}>
- {trace.name}
- </option>
- ))}
- </select>
- </div>
- <div className="form-group">
- <label className="form-control-label">Load sampling fraction</label>
- <input
- type="number"
- className="form-control"
- required
- ref={(traceLoadInput) => (this.traceLoadInput = traceLoadInput)}
- />
- </div>
- <h4>Topology</h4>
- <div className="form-group">
- <label className="form-control-label">Topology</label>
- <select
- className="form-control"
- ref={(topologySelect) => (this.topologySelect = topologySelect)}
- >
- {this.props.topologies.map((topology) => (
- <option value={topology._id} key={topology._id}>
- {topology.name}
- </option>
- ))}
- </select>
- </div>
- <h4>Operational Phenomena</h4>
- <div className="form-check">
- <label className="form-check-label">
- <input
- type="checkbox"
- className="form-check-input"
- ref={(failuresCheckbox) => (this.failuresCheckbox = failuresCheckbox)}
- />
- <span className="ml-2">Enable failures</span>
- </label>
- </div>
- <div className="form-check">
- <label className="form-check-label">
- <input
- type="checkbox"
- className="form-check-input"
- ref={(performanceInterferenceCheckbox) =>
- (this.performanceInterferenceCheckbox = performanceInterferenceCheckbox)
- }
- />
- <span className="ml-2">Enable performance interference</span>
- </label>
- </div>
- <div className="form-group">
- <label className="form-control-label">Scheduler</label>
- <select
- className="form-control"
- ref={(schedulerSelect) => (this.schedulerSelect = schedulerSelect)}
- >
- {this.props.schedulers.map((scheduler) => (
- <option value={scheduler.name} key={scheduler.name}>
- {scheduler.name}
- </option>
- ))}
- </select>
- </div>
- </form>
- </Modal>
- )
- }
+ <FormGroup>
+ <Label for="name">Name</Label>
+ <Input
+ name="name"
+ type="text"
+ required
+ disabled={currentPortfolioScenarioIds.length === 0}
+ defaultValue={currentPortfolioScenarioIds.length === 0 ? 'Base scenario' : ''}
+ innerRef={textInput}
+ />
+ </FormGroup>
+ <h4>Trace</h4>
+ <FormGroup>
+ <Label for="trace">Trace</Label>
+ <Input name="trace" type="select" innerRef={traceSelect}>
+ {traces.map((trace) => (
+ <option value={trace._id} key={trace._id}>
+ {trace.name}
+ </option>
+ ))}
+ </Input>
+ </FormGroup>
+ <FormGroup>
+ <Label for="trace-load">Load sampling fraction</Label>
+ <Input
+ name="trace-load"
+ type="number"
+ innerRef={traceLoadInput}
+ required
+ defaultValue="1"
+ min="0"
+ max="1"
+ step="0.1"
+ />
+ </FormGroup>
+ <h4>Topology</h4>
+ <div className="form-group">
+ <Label for="topology">Topology</Label>
+ <Input name="topology" type="select" innerRef={topologySelect}>
+ {topologies.map((topology) => (
+ <option value={topology._id} key={topology._id}>
+ {topology.name}
+ </option>
+ ))}
+ </Input>
+ </div>
+ <h4>Operational Phenomena</h4>
+ <FormGroup check>
+ <Label check for="failures">
+ <Input type="checkbox" name="failures" innerRef={failuresCheckbox} />{' '}
+ <span className="ml-2">Enable failures</span>
+ </Label>
+ </FormGroup>
+ <FormGroup check>
+ <Label check for="perf-interference">
+ <Input type="checkbox" name="perf-interference" innerRef={performanceInterferenceCheckbox} />{' '}
+ <span className="ml-2">Enable performance interference</span>
+ </Label>
+ </FormGroup>
+ <FormGroup>
+ <Label for="scheduler">Scheduler</Label>
+ <Input name="scheduler" type="select" innerRef={schedulerSelect}>
+ {schedulers.map((scheduler) => (
+ <option value={scheduler.name} key={scheduler.name}>
+ {scheduler.name}
+ </option>
+ ))}
+ </Input>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewScenarioModalComponent.propTypes = {
+ show: PropTypes.bool.isRequired,
+ currentPortfolioId: PropTypes.string.isRequired,
+ currentPortfolioScenarioIds: PropTypes.arrayOf(PropTypes.string),
+ traces: PropTypes.arrayOf(Shapes.Trace),
+ topologies: PropTypes.arrayOf(Shapes.Topology),
+ schedulers: PropTypes.arrayOf(Shapes.Scheduler),
+ callback: PropTypes.func.isRequired,
}
export default NewScenarioModalComponent
diff --git a/frontend/src/components/modals/custom-components/NewTopologyModalComponent.js b/frontend/src/components/modals/custom-components/NewTopologyModalComponent.js
index 8a625b13..b20ec13b 100644
--- a/frontend/src/components/modals/custom-components/NewTopologyModalComponent.js
+++ b/frontend/src/components/modals/custom-components/NewTopologyModalComponent.js
@@ -1,90 +1,65 @@
import PropTypes from 'prop-types'
-import React from 'react'
+import { Form, FormGroup, Input, Label } from 'reactstrap'
+import React, { useRef } from 'react'
import Shapes from '../../../shapes'
import Modal from '../Modal'
-class NewTopologyModalComponent extends React.Component {
- static propTypes = {
- show: PropTypes.bool.isRequired,
- topologies: PropTypes.arrayOf(Shapes.Topology),
- onCreateTopology: PropTypes.func.isRequired,
- onDuplicateTopology: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
+const NewTopologyModalComponent = ({ show, onCreateTopology, onDuplicateTopology, onCancel, topologies }) => {
+ const textInput = useRef(null)
+ const originTopology = useRef(null)
+
+ const onCreate = () => {
+ onCreateTopology(textInput.current.value)
}
- reset() {
- if (this.textInput) {
- this.textInput.value = ''
- this.originTopology.selectedIndex = 0
- }
+ const onDuplicate = () => {
+ onDuplicateTopology(textInput.current.value, originTopology.current.value)
}
- onSubmit() {
- if (this.originTopology.selectedIndex === 0) {
- this.onCreate()
+ const onSubmit = () => {
+ if (originTopology.current.selectedIndex === 0) {
+ onCreate()
} else {
- this.onDuplicate()
+ onDuplicate()
}
}
- onCreate() {
- this.props.onCreateTopology(this.textInput.value)
- this.reset()
- }
-
- onDuplicate() {
- this.props.onDuplicateTopology(this.textInput.value, this.originTopology.value)
- this.reset()
- }
-
- onCancel() {
- this.props.onCancel()
- this.reset()
- }
-
- render() {
- return (
- <Modal
- title="New Topology"
- show={this.props.show}
- onSubmit={this.onSubmit.bind(this)}
- onCancel={this.onCancel.bind(this)}
+ return (
+ <Modal title="New Topology" show={show} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault()
+ onSubmit()
+ }}
>
- <form
- onSubmit={(e) => {
- e.preventDefault()
- this.onSubmit()
- }}
- >
- <div className="form-group">
- <label className="form-control-label">Name</label>
- <input
- type="text"
- className="form-control"
- required
- ref={(textInput) => (this.textInput = textInput)}
- />
- </div>
- <div className="form-group">
- <label className="form-control-label">Topology to duplicate</label>
- <select
- className="form-control"
- ref={(originTopology) => (this.originTopology = originTopology)}
- >
- <option value={-1} key={-1}>
- None - start from scratch
+ <FormGroup>
+ <Label for="name">Name</Label>
+ <Input name="name" type="text" required innerRef={textInput} />
+ </FormGroup>
+ <FormGroup>
+ <Label for="origin">Topology to duplicate</Label>
+ <Input name="origin" type="select" innerRef={originTopology}>
+ <option value={-1} key={-1}>
+ None - start from scratch
+ </option>
+ {topologies.map((topology) => (
+ <option value={topology._id} key={topology._id}>
+ {topology.name}
</option>
- {this.props.topologies.map((topology) => (
- <option value={topology._id} key={topology._id}>
- {topology.name}
- </option>
- ))}
- </select>
- </div>
- </form>
- </Modal>
- )
- }
+ ))}
+ </Input>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewTopologyModalComponent.propTypes = {
+ show: PropTypes.bool.isRequired,
+ topologies: PropTypes.arrayOf(Shapes.Topology),
+ onCreateTopology: PropTypes.func.isRequired,
+ onDuplicateTopology: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
}
export default NewTopologyModalComponent
diff --git a/frontend/src/components/navigation/AppNavbarComponent.js b/frontend/src/components/navigation/AppNavbarComponent.js
index 7a1c2462..c5de3d0b 100644
--- a/frontend/src/components/navigation/AppNavbarComponent.js
+++ b/frontend/src/components/navigation/AppNavbarComponent.js
@@ -1,22 +1,23 @@
import React from 'react'
import FontAwesome from 'react-fontawesome'
import { Link } from 'react-router-dom'
+import { NavLink } from 'reactstrap'
import Navbar, { NavItem } from './Navbar'
import './Navbar.sass'
const AppNavbarComponent = ({ project, fullWidth }) => (
<Navbar fullWidth={fullWidth}>
<NavItem route="/projects">
- <Link className="nav-link" title="My Projects" to="/projects">
+ <NavLink tag={Link} title="My Projects" to="/projects">
<FontAwesome name="list" className="mr-2" />
My Projects
- </Link>
+ </NavLink>
</NavItem>
{project ? (
<NavItem>
- <Link className="nav-link" title="Current Project" to={`/projects/${project._id}`}>
+ <NavLink tag={Link} title="Current Project" to={`/projects/${project._id}`}>
<span>{project.name}</span>
- </Link>
+ </NavLink>
</NavItem>
) : undefined}
</Navbar>
diff --git a/frontend/src/components/navigation/HomeNavbar.js b/frontend/src/components/navigation/HomeNavbar.js
index e22933af..08d222ea 100644
--- a/frontend/src/components/navigation/HomeNavbar.js
+++ b/frontend/src/components/navigation/HomeNavbar.js
@@ -1,13 +1,12 @@
import React from 'react'
+import { NavItem, NavLink } from 'reactstrap'
import Navbar from './Navbar'
import './Navbar.sass'
const ScrollNavItem = ({ id, name }) => (
- <li className="nav-item">
- <a className="nav-link" href={id}>
- {name}
- </a>
- </li>
+ <NavItem>
+ <NavLink href={id}>{name}</NavLink>
+ </NavItem>
)
const HomeNavbar = () => (
diff --git a/frontend/src/components/navigation/LogoutButton.js b/frontend/src/components/navigation/LogoutButton.js
index e3de2ec7..78b02b44 100644
--- a/frontend/src/components/navigation/LogoutButton.js
+++ b/frontend/src/components/navigation/LogoutButton.js
@@ -2,11 +2,12 @@ import PropTypes from 'prop-types'
import React from 'react'
import FontAwesome from 'react-fontawesome'
import { Link } from 'react-router-dom'
+import { NavLink } from 'reactstrap'
const LogoutButton = ({ onLogout }) => (
- <Link className="logout nav-link" title="Sign out" to="#" onClick={onLogout}>
+ <NavLink tag={Link} className="logout" title="Sign out" to="#" onClick={onLogout}>
<FontAwesome name="power-off" size="lg" />
- </Link>
+ </NavLink>
)
LogoutButton.propTypes = {
diff --git a/frontend/src/components/navigation/Navbar.js b/frontend/src/components/navigation/Navbar.js
index 164a2309..55f98900 100644
--- a/frontend/src/components/navigation/Navbar.js
+++ b/frontend/src/components/navigation/Navbar.js
@@ -1,6 +1,15 @@
-import classNames from 'classnames'
-import React from 'react'
-import { Link, withRouter } from 'react-router-dom'
+import React, { useState } from 'react'
+import { Link, useLocation } from 'react-router-dom'
+import {
+ Navbar as RNavbar,
+ NavItem as RNavItem,
+ NavLink,
+ NavbarBrand,
+ NavbarToggler,
+ Collapse,
+ Nav,
+ Container,
+} from 'reactstrap'
import { userIsLoggedIn } from '../../auth/index'
import Login from '../../containers/auth/Login'
import Logout from '../../containers/auth/Logout'
@@ -9,9 +18,6 @@ import './Navbar.sass'
export const NAVBAR_HEIGHT = 60
-export const NavItem = withRouter((props) => <NavItemWithoutRoute {...props} />)
-export const LoggedInSection = withRouter((props) => <LoggedInSectionWithoutRoute {...props} />)
-
const GitHubLink = () => (
<a
href="https://github.com/atlarge-research/opendc"
@@ -22,64 +28,65 @@ const GitHubLink = () => (
</a>
)
-const NavItemWithoutRoute = ({ route, location, children }) => (
- <li className={classNames('nav-item clickable', location.pathname === route ? 'active' : undefined)}>{children}</li>
-)
+export const NavItem = ({ route, children }) => {
+ const location = useLocation()
+ return <RNavItem active={location.pathname === route}>{children}</RNavItem>
+}
-const LoggedInSectionWithoutRoute = ({ location }) => (
- <ul className="navbar-nav auth-links">
- {userIsLoggedIn() ? (
- [
- location.pathname === '/' ? (
- <NavItem route="/projects" key="projects">
- <Link className="nav-link" title="My Projects" to="/projects">
- My Projects
- </Link>
- </NavItem>
- ) : (
- <NavItem route="/profile" key="profile">
- <Link className="nav-link" title="My Profile" to="/profile">
- <ProfileName />
- </Link>
- </NavItem>
- ),
- <NavItem route="logout" key="logout">
- <Logout />
- </NavItem>,
- ]
- ) : (
- <NavItem route="login">
- <GitHubLink />
- <Login visible={true} />
- </NavItem>
- )}
- </ul>
-)
+export const LoggedInSection = () => {
+ const location = useLocation()
+ return (
+ <Nav navbar className="auth-links">
+ {userIsLoggedIn() ? (
+ [
+ location.pathname === '/' ? (
+ <NavItem route="/projects" key="projects">
+ <NavLink tag={Link} title="My Projects" to="/projects">
+ My Projects
+ </NavLink>
+ </NavItem>
+ ) : (
+ <NavItem route="/profile" key="profile">
+ <NavLink tag={Link} title="My Profile" to="/profile">
+ <ProfileName />
+ </NavLink>
+ </NavItem>
+ ),
+ <NavItem route="logout" key="logout">
+ <Logout />
+ </NavItem>,
+ ]
+ ) : (
+ <NavItem route="login">
+ <GitHubLink />
+ <Login visible={true} />
+ </NavItem>
+ )}
+ </Nav>
+ )
+}
-const Navbar = ({ fullWidth, children }) => (
- <nav className="navbar fixed-top navbar-expand-lg navbar-light bg-faded" id="navbar">
- <div className={fullWidth ? 'container-fluid' : 'container'}>
- <button
- className="navbar-toggler navbar-toggler-right"
- type="button"
- data-toggle="collapse"
- data-target="#navbarSupportedContent"
- aria-controls="navbarSupportedContent"
- aria-expanded="false"
- aria-label="Toggle navigation"
- >
- <span className="navbar-toggler-icon" />
- </button>
- <Link className="navbar-brand opendc-brand" to="/" title="OpenDC" onClick={() => window.scrollTo(0, 0)}>
- <img src="/img/logo.png" alt="OpenDC" />
- </Link>
+const Navbar = ({ fullWidth, children }) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const toggle = () => setIsOpen(!isOpen)
- <div className="collapse navbar-collapse" id="navbarSupportedContent">
- <ul className="navbar-nav mr-auto">{children}</ul>
- <LoggedInSection />
- </div>
- </div>
- </nav>
-)
+ return (
+ <RNavbar fixed="top" color="light" light expand="lg" id="navbar">
+ <Container fluid={fullWidth}>
+ <NavbarToggler onClick={toggle} />
+ <NavbarBrand tag={Link} to="/" title="OpenDC" className="opendc-brand">
+ <img src="/img/logo.png" alt="OpenDC" />
+ </NavbarBrand>
+
+ <Collapse isOpen={isOpen} navbar>
+ <Nav className="mr-auto" navbar>
+ {children}
+ </Nav>
+ <LoggedInSection />
+ </Collapse>
+ </Container>
+ </RNavbar>
+ )
+}
export default Navbar
diff --git a/frontend/src/containers/modals/NewScenarioModal.js b/frontend/src/containers/modals/NewScenarioModal.js
index 225ae321..7d774fa4 100644
--- a/frontend/src/containers/modals/NewScenarioModal.js
+++ b/frontend/src/containers/modals/NewScenarioModal.js
@@ -39,6 +39,7 @@ const mapDispatchToProps = (dispatch) => {
})
)
}
+
dispatch(closeNewScenarioModal())
},
}
diff --git a/frontend/src/sagas/objects.js b/frontend/src/sagas/objects.js
index ebce1f0a..313d9976 100644
--- a/frontend/src/sagas/objects.js
+++ b/frontend/src/sagas/objects.js
@@ -36,8 +36,8 @@ function* fetchAndStoreObject(objectType, id, apiCall) {
function* fetchAndStoreObjects(objectType, apiCall) {
const objects = yield apiCall
- for (let index in objects) {
- yield put(addToStore(objectType, objects[index]))
+ for (let object of objects) {
+ yield put(addToStore(objectType, object))
}
return objects
}
@@ -161,11 +161,11 @@ export const getAllRooms = function* (roomIds, keepIds) {
let rooms = []
- for (let i in roomIds) {
- let tiles = yield getAllRoomTiles(roomStore[roomIds[i]], keepIds)
+ for (let id of roomIds) {
+ let tiles = yield getAllRoomTiles(roomStore[id], keepIds)
rooms.push({
- _id: keepIds ? i : undefined,
- name: roomStore[roomIds[i]].name,
+ _id: keepIds ? id : undefined,
+ name: roomStore[id].name,
tiles: tiles,
})
}
@@ -175,8 +175,8 @@ export const getAllRooms = function* (roomIds, keepIds) {
export const getAllRoomTiles = function* (roomStore, keepIds) {
let tiles = []
- for (let i in roomStore.tileIds) {
- tiles.push(yield getTileById(roomStore.tileIds[i], keepIds))
+ for (let id of roomStore.tileIds) {
+ tiles.push(yield getTileById(id, keepIds))
}
return tiles
}
@@ -221,9 +221,9 @@ export const fetchAndStoreAllTraces = () => fetchAndStoreObjects('trace', call(g
export const fetchAndStoreAllSchedulers = function* () {
const objects = yield call(getAllSchedulers)
- for (let index in objects) {
- objects[index]._id = objects[index].name
- yield put(addToStore('scheduler', objects[index]))
+ for (let object of objects) {
+ object._id = object.name
+ yield put(addToStore('scheduler', object))
}
return objects
}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index b77c38f6..7398bf38 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1069,14 +1069,14 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
version "7.10.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.2.0":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.2.0":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740"
integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==
@@ -5463,7 +5463,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
-hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -7428,13 +7428,12 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mini-create-react-context@^0.3.0:
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
- integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.3.tgz#b1b2bc6604d3a6c5d9752bad7692615410ebb38e"
+ integrity sha512-TtF6hZE59SGmS4U8529qB+jJFeW6asTLDIpPgvPLSCsooAwJS7QprHIFTqv9/Qh3NdLwQxFYgiHX5lqb6jqzPA==
dependencies:
- "@babel/runtime" "^7.4.0"
- gud "^1.0.0"
- tiny-warning "^1.0.2"
+ "@babel/runtime" "^7.12.1"
+ tiny-warning "^1.0.3"
mini-css-extract-plugin@0.9.0:
version "0.9.0"
@@ -9463,7 +9462,7 @@ react-google-login@~5.1.14:
"@types/react" "*"
prop-types "^15.6.0"
-react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
+react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -9505,15 +9504,15 @@ react-reconciler@^0.25.1:
scheduler "^0.19.1"
react-redux@~7.2.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
- integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
+ integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
dependencies:
- "@babel/runtime" "^7.5.5"
- hoist-non-react-statics "^3.3.0"
+ "@babel/runtime" "^7.12.1"
+ hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
- react-is "^16.9.0"
+ react-is "^16.13.1"
react-resize-detector@^2.3.0:
version "2.3.0"
@@ -9873,11 +9872,16 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
+regenerator-runtime@^0.13.3:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+regenerator-runtime@^0.13.4:
+ version "0.13.7"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+ integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
regenerator-transform@^0.14.2:
version "0.14.4"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
@@ -11251,7 +11255,7 @@ tiny-invariant@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
-tiny-warning@^1.0.0, tiny-warning@^1.0.2:
+tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
diff --git a/simulator/.dockerignore b/simulator/.dockerignore
index 816d338c..8cbb9e5e 100644
--- a/simulator/.dockerignore
+++ b/simulator/.dockerignore
@@ -6,5 +6,5 @@
.idea_modules/
.gradle
-**/build/
+**/build
diff --git a/simulator/Dockerfile b/simulator/Dockerfile
index 880af95d..34280a17 100644
--- a/simulator/Dockerfile
+++ b/simulator/Dockerfile
@@ -1,31 +1,17 @@
-FROM openjdk:14-slim AS staging
+FROM openjdk:15-slim
MAINTAINER OpenDC Maintainers <opendc@atlarge-research.com>
-# Build staging artifacts for dependency caching
-COPY ./ /app
-WORKDIR /app
-RUN mkdir /staging \
- && cp -r buildSrc/ /staging \
- && cp gradle.properties /staging 2>/dev/null | true \
- && find -name "*.gradle.kts" | xargs cp --parents -t /staging
-
-FROM openjdk:14-slim AS builder
-
# Obtain (cache) Gradle wrapper
COPY gradlew /app/
COPY gradle /app/gradle
WORKDIR /app
RUN ./gradlew --version
-# Install (cache) project dependencies only
-COPY --from=staging /staging/ /app/
-RUN ./gradlew clean build --no-daemon > /dev/null 2>&1 || true
-
# Build project
COPY ./ /app/
RUN ./gradlew --no-daemon :opendc-runner-web:installDist
-FROM openjdk:14-slim
-COPY --from=builder /app/opendc-runner-web/build/install /app
+FROM openjdk:15-slim
+COPY --from=0 /app/opendc-runner-web/build/install /app
WORKDIR /app
CMD opendc-runner-web/bin/opendc-runner-web
diff --git a/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts b/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts
index 452db573..bbecf346 100644
--- a/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts
+++ b/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts
@@ -44,6 +44,10 @@ kotlin {
explicitApi()
}
+jacoco {
+ toolVersion = "0.8.6"
+}
+
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
diff --git a/simulator/gradle/wrapper/gradle-wrapper.properties b/simulator/gradle/wrapper/gradle-wrapper.properties
index 12d38de6..be52383e 100644
--- a/simulator/gradle/wrapper/gradle-wrapper.properties
+++ b/simulator/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/simulator/opendc-runner-web/build.gradle.kts b/simulator/opendc-runner-web/build.gradle.kts
index cf437843..a46e430f 100644
--- a/simulator/opendc-runner-web/build.gradle.kts
+++ b/simulator/opendc-runner-web/build.gradle.kts
@@ -42,12 +42,11 @@ dependencies {
implementation("com.github.ajalt:clikt:2.8.0")
implementation("io.github.microutils:kotlin-logging:1.7.10")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") {
+ exclude("org.jetbrains.kotlin", module = "kotlin-reflect")
+ }
implementation("org.mongodb:mongodb-driver-sync:4.0.5")
- implementation("org.apache.spark:spark-sql_2.12:3.0.0") {
- exclude(group = "org.slf4j", module = "slf4j-log4j12")
- exclude(group = "log4j")
- }
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.1")
runtimeOnly("org.apache.logging.log4j:log4j-1.2-api:2.13.1")
diff --git a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt
index 26577ef2..80b3bb20 100644
--- a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt
+++ b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt
@@ -38,12 +38,12 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.test.TestCoroutineScope
import mu.KotlinLogging
import org.bson.Document
+import org.bson.types.ObjectId
import org.opendc.compute.simulator.allocation.*
import org.opendc.experiments.sc20.experiment.attachMonitor
import org.opendc.experiments.sc20.experiment.createFailureDomain
import org.opendc.experiments.sc20.experiment.createProvisioner
import org.opendc.experiments.sc20.experiment.model.Workload
-import org.opendc.experiments.sc20.experiment.monitor.ParquetExperimentMonitor
import org.opendc.experiments.sc20.experiment.processTrace
import org.opendc.experiments.sc20.trace.Sc20ParquetTraceReader
import org.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader
@@ -123,27 +123,6 @@ public class RunnerCli : CliktCommand(name = "runner") {
.defaultLazy { File("traces/") }
/**
- * The path to the output directory.
- */
- private val outputPath by option(
- "--output",
- help = "path to the results directory",
- envvar = "OPENDC_OUTPUT"
- )
- .file(canBeFile = false)
- .defaultLazy { File("results/") }
-
- /**
- * The Spark master to connect to.
- */
- private val spark by option(
- "--spark",
- help = "Spark master to connect to",
- envvar = "OPENDC_SPARK"
- )
- .default("local[*]")
-
- /**
* Connect to the user-specified database.
*/
private fun createDatabase(): MongoDatabase {
@@ -164,8 +143,8 @@ public class RunnerCli : CliktCommand(name = "runner") {
/**
* Run a single scenario.
*/
- private suspend fun runScenario(portfolio: Document, scenario: Document, topologies: MongoCollection<Document>) {
- val id = scenario.getString("_id")
+ private suspend fun runScenario(portfolio: Document, scenario: Document, topologies: MongoCollection<Document>): List<WebExperimentMonitor.Result> {
+ val id = scenario.getObjectId("_id")
logger.info { "Constructing performance interference model" }
@@ -188,12 +167,14 @@ public class RunnerCli : CliktCommand(name = "runner") {
val targets = portfolio.get("targets", Document::class.java)
- repeat(targets.getInteger("repeatsPerScenario")) {
+ val results = (0..targets.getInteger("repeatsPerScenario") - 1).map {
logger.info { "Starting repeat $it" }
runRepeat(scenario, it, topologies, traceReader, performanceInterferenceReader)
}
logger.info { "Finished simulation for scenario $id" }
+
+ return results
}
/**
@@ -205,8 +186,7 @@ public class RunnerCli : CliktCommand(name = "runner") {
topologies: MongoCollection<Document>,
traceReader: Sc20RawParquetTraceReader,
performanceInterferenceReader: Sc20PerformanceInterferenceReader?
- ) {
- val id = scenario.getString("_id")
+ ): WebExperimentMonitor.Result {
val seed = repeat
val traceDocument = scenario.get("trace", Document::class.java)
val workloadName = traceDocument.getString("traceId")
@@ -240,13 +220,9 @@ public class RunnerCli : CliktCommand(name = "runner") {
Workload(workloadName, workloadFraction),
seed
)
- val topologyId = scenario.getEmbedded(listOf("topology", "topologyId"), String::class.java)
+ val topologyId = scenario.getEmbedded(listOf("topology", "topologyId"), ObjectId::class.java)
val environment = TopologyParser(topologies, topologyId)
- val monitor = ParquetExperimentMonitor(
- outputPath,
- "scenario_id=$id/run_id=$repeat",
- 4096
- )
+ val monitor = WebExperimentMonitor()
testScope.launch {
val (bareMetalProvisioner, scheduler) = createProvisioner(
@@ -292,9 +268,12 @@ public class RunnerCli : CliktCommand(name = "runner") {
try {
testScope.advanceUntilIdle()
+ testScope.uncaughtExceptions.forEach { it.printStackTrace() }
} finally {
monitor.close()
}
+
+ return monitor.getResult()
}
private val POLL_INTERVAL = 5000L // ms = 5 s
@@ -308,9 +287,6 @@ public class RunnerCli : CliktCommand(name = "runner") {
val portfolios = database.getCollection("portfolios")
val topologies = database.getCollection("topologies")
- logger.info { "Launching Spark" }
- val resultProcessor = ResultProcessor(spark, outputPath)
-
logger.info { "Watching for queued scenarios" }
while (true) {
@@ -321,7 +297,7 @@ public class RunnerCli : CliktCommand(name = "runner") {
continue
}
- val id = scenario.getString("_id")
+ val id = scenario.getObjectId("_id")
logger.info { "Found queued scenario $id: attempting to claim" }
@@ -340,13 +316,12 @@ public class RunnerCli : CliktCommand(name = "runner") {
}
try {
- val portfolio = portfolios.find(Filters.eq("_id", scenario.getString("portfolioId"))).first()!!
- runScenario(portfolio, scenario, topologies)
+ val portfolio = portfolios.find(Filters.eq("_id", scenario.getObjectId("portfolioId"))).first()!!
+ val results = runScenario(portfolio, scenario, topologies)
- logger.info { "Starting result processing" }
+ logger.info { "Writing results to database" }
- val result = resultProcessor.process(id)
- manager.finish(id, result)
+ manager.finish(id, results)
logger.info { "Successfully finished scenario $id" }
} catch (e: Exception) {
diff --git a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ResultProcessor.kt b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ResultProcessor.kt
deleted file mode 100644
index f9a3cd2b..00000000
--- a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ResultProcessor.kt
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (c) 2020 AtLarge Research
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.opendc.runner.web
-
-import org.apache.spark.sql.Column
-import org.apache.spark.sql.Dataset
-import org.apache.spark.sql.Row
-import org.apache.spark.sql.SparkSession
-import org.apache.spark.sql.functions.*
-import java.io.File
-
-/**
- * A helper class for processing the experiment results using Apache Spark.
- */
-public class ResultProcessor(private val master: String, private val outputPath: File) {
- /**
- * Process the results of the scenario with the given [id].
- */
- public fun process(id: String): Result {
- val spark = SparkSession.builder()
- .master(master)
- .appName("opendc-simulator-$id")
- .config("spark.driver.bindAddress", "0.0.0.0") // Needed to allow the worker to connect to driver
- .orCreate
-
- try {
- val hostMetrics = spark.read().parquet(File(outputPath, "host-metrics/scenario_id=$id").path)
- val provisionerMetrics = spark.read().parquet(File(outputPath, "provisioner-metrics/scenario_id=$id").path)
- val res = aggregate(hostMetrics, provisionerMetrics).first()
-
- return Result(
- res.getList<Long>(1),
- res.getList<Long>(2),
- res.getList<Long>(3),
- res.getList<Long>(4),
- res.getList<Double>(5),
- res.getList<Double>(6),
- res.getList<Double>(7),
- res.getList<Int>(8),
- res.getList<Long>(9),
- res.getList<Long>(10),
- res.getList<Long>(11),
- res.getList<Int>(12),
- res.getList<Int>(13),
- res.getList<Int>(14),
- res.getList<Int>(15)
- )
- } finally {
- spark.close()
- }
- }
-
- public data class Result(
- public val totalRequestedBurst: List<Long>,
- public val totalGrantedBurst: List<Long>,
- public val totalOvercommittedBurst: List<Long>,
- public val totalInterferedBurst: List<Long>,
- public val meanCpuUsage: List<Double>,
- public val meanCpuDemand: List<Double>,
- public val meanNumDeployedImages: List<Double>,
- public val maxNumDeployedImages: List<Int>,
- public val totalPowerDraw: List<Long>,
- public val totalFailureSlices: List<Long>,
- public val totalFailureVmSlices: List<Long>,
- public val totalVmsSubmitted: List<Int>,
- public val totalVmsQueued: List<Int>,
- public val totalVmsFinished: List<Int>,
- public val totalVmsFailed: List<Int>
- )
-
- /**
- * Perform aggregation of the experiment results.
- */
- private fun aggregate(hostMetrics: Dataset<Row>, provisionerMetrics: Dataset<Row>): Dataset<Row> {
- // Extrapolate the duration of the entries to span the entire trace
- val hostMetricsExtra = hostMetrics
- .withColumn("slice_counts", floor(col("duration") / lit(sliceLength)))
- .withColumn("power_draw", col("power_draw") * col("slice_counts"))
- .withColumn("state_int", states[col("state")])
- .withColumn("state_opposite_int", oppositeStates[col("state")])
- .withColumn("cpu_usage", col("cpu_usage") * col("slice_counts") * col("state_opposite_int"))
- .withColumn("cpu_demand", col("cpu_demand") * col("slice_counts"))
- .withColumn("failure_slice_count", col("slice_counts") * col("state_int"))
- .withColumn("failure_vm_slice_count", col("slice_counts") * col("state_int") * col("vm_count"))
-
- // Process all data in a single run
- val hostMetricsGrouped = hostMetricsExtra.groupBy("run_id")
-
- // Aggregate the summed total metrics
- val systemMetrics = hostMetricsGrouped.agg(
- sum("requested_burst").alias("total_requested_burst"),
- sum("granted_burst").alias("total_granted_burst"),
- sum("overcommissioned_burst").alias("total_overcommitted_burst"),
- sum("interfered_burst").alias("total_interfered_burst"),
- sum("power_draw").alias("total_power_draw"),
- sum("failure_slice_count").alias("total_failure_slices"),
- sum("failure_vm_slice_count").alias("total_failure_vm_slices")
- )
-
- // Aggregate metrics per host
- val hvMetrics = hostMetrics
- .groupBy("run_id", "host_id")
- .agg(
- sum("cpu_usage").alias("mean_cpu_usage"),
- sum("cpu_demand").alias("mean_cpu_demand"),
- avg("vm_count").alias("mean_num_deployed_images"),
- count(lit(1)).alias("num_rows")
- )
- .withColumn("mean_cpu_usage", col("mean_cpu_usage") / col("num_rows"))
- .withColumn("mean_cpu_demand", col("mean_cpu_demand") / col("num_rows"))
- .groupBy("run_id")
- .agg(
- avg("mean_cpu_usage").alias("mean_cpu_usage"),
- avg("mean_cpu_demand").alias("mean_cpu_demand"),
- avg("mean_num_deployed_images").alias("mean_num_deployed_images"),
- max("mean_num_deployed_images").alias("max_num_deployed_images")
- )
-
- // Group the provisioner metrics per run
- val provisionerMetricsGrouped = provisionerMetrics.groupBy("run_id")
-
- // Aggregate the provisioner metrics
- val provisionerMetricsAggregated = provisionerMetricsGrouped.agg(
- max("vm_total_count").alias("total_vms_submitted"),
- max("vm_waiting_count").alias("total_vms_queued"),
- max("vm_active_count").alias("total_vms_running"),
- max("vm_inactive_count").alias("total_vms_finished"),
- max("vm_failed_count").alias("total_vms_failed")
- )
-
- // Join the results into a single data frame
- return systemMetrics
- .join(hvMetrics, "run_id")
- .join(provisionerMetricsAggregated, "run_id")
- .select(
- col("total_requested_burst"),
- col("total_granted_burst"),
- col("total_overcommitted_burst"),
- col("total_interfered_burst"),
- col("mean_cpu_usage"),
- col("mean_cpu_demand"),
- col("mean_num_deployed_images"),
- col("max_num_deployed_images"),
- col("total_power_draw"),
- col("total_failure_slices"),
- col("total_failure_vm_slices"),
- col("total_vms_submitted"),
- col("total_vms_queued"),
- col("total_vms_finished"),
- col("total_vms_failed")
- )
- .groupBy(lit(1))
- .agg(
- // TODO Check if order of values is correct
- collect_list(col("total_requested_burst")).alias("total_requested_burst"),
- collect_list(col("total_granted_burst")).alias("total_granted_burst"),
- collect_list(col("total_overcommitted_burst")).alias("total_overcommitted_burst"),
- collect_list(col("total_interfered_burst")).alias("total_interfered_burst"),
- collect_list(col("mean_cpu_usage")).alias("mean_cpu_usage"),
- collect_list(col("mean_cpu_demand")).alias("mean_cpu_demand"),
- collect_list(col("mean_num_deployed_images")).alias("mean_num_deployed_images"),
- collect_list(col("max_num_deployed_images")).alias("max_num_deployed_images"),
- collect_list(col("total_power_draw")).alias("total_power_draw"),
- collect_list(col("total_failure_slices")).alias("total_failure_slices"),
- collect_list(col("total_failure_vm_slices")).alias("total_failure_vm_slices"),
- collect_list(col("total_vms_submitted")).alias("total_vms_submitted"),
- collect_list(col("total_vms_queued")).alias("total_vms_queued"),
- collect_list(col("total_vms_finished")).alias("total_vms_finished"),
- collect_list(col("total_vms_failed")).alias("total_vms_failed")
- )
- }
-
- // Spark helper functions
- private operator fun Column.times(other: Column): Column = `$times`(other)
- private operator fun Column.div(other: Column): Column = `$div`(other)
- private operator fun Column.get(other: Column): Column = this.apply(other)
-
- private val sliceLength = 5 * 60 * 1000
- private val states = map(
- lit("ERROR"),
- lit(1),
- lit("ACTIVE"),
- lit(0),
- lit("SHUTOFF"),
- lit(0)
- )
- private val oppositeStates = map(
- lit("ERROR"),
- lit(0),
- lit("ACTIVE"),
- lit(1),
- lit("SHUTOFF"),
- lit(1)
- )
-}
diff --git a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ScenarioManager.kt b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ScenarioManager.kt
index 504fccdc..a3907051 100644
--- a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ScenarioManager.kt
+++ b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/ScenarioManager.kt
@@ -26,6 +26,7 @@ import com.mongodb.client.MongoCollection
import com.mongodb.client.model.Filters
import com.mongodb.client.model.Updates
import org.bson.Document
+import org.bson.types.ObjectId
import java.time.Instant
/**
@@ -44,7 +45,7 @@ public class ScenarioManager(private val collection: MongoCollection<Document>)
/**
* Claim the scenario in the database with the specified id.
*/
- public fun claim(id: String): Boolean {
+ public fun claim(id: ObjectId): Boolean {
val res = collection.findOneAndUpdate(
Filters.and(
Filters.eq("_id", id),
@@ -61,7 +62,7 @@ public class ScenarioManager(private val collection: MongoCollection<Document>)
/**
* Update the heartbeat of the specified scenario.
*/
- public fun heartbeat(id: String) {
+ public fun heartbeat(id: ObjectId) {
collection.findOneAndUpdate(
Filters.and(
Filters.eq("_id", id),
@@ -74,7 +75,7 @@ public class ScenarioManager(private val collection: MongoCollection<Document>)
/**
* Mark the scenario as failed.
*/
- public fun fail(id: String) {
+ public fun fail(id: ObjectId) {
collection.findOneAndUpdate(
Filters.eq("_id", id),
Updates.combine(
@@ -87,28 +88,27 @@ public class ScenarioManager(private val collection: MongoCollection<Document>)
/**
* Persist the specified results.
*/
- public fun finish(id: String, result: ResultProcessor.Result) {
+ public fun finish(id: ObjectId, results: List<WebExperimentMonitor.Result>) {
collection.findOneAndUpdate(
Filters.eq("_id", id),
Updates.combine(
Updates.set("simulation.state", "FINISHED"),
Updates.unset("simulation.time"),
- Updates.set("results.total_requested_burst", result.totalRequestedBurst),
- Updates.set("results.total_granted_burst", result.totalGrantedBurst),
- Updates.set("results.total_overcommitted_burst", result.totalOvercommittedBurst),
- Updates.set("results.total_interfered_burst", result.totalInterferedBurst),
- Updates.set("results.mean_cpu_usage", result.meanCpuUsage),
- Updates.set("results.mean_cpu_demand", result.meanCpuDemand),
- Updates.set("results.mean_num_deployed_images", result.meanNumDeployedImages),
- Updates.set("results.max_num_deployed_images", result.maxNumDeployedImages),
- Updates.set("results.max_num_deployed_images", result.maxNumDeployedImages),
- Updates.set("results.total_power_draw", result.totalPowerDraw),
- Updates.set("results.total_failure_slices", result.totalFailureSlices),
- Updates.set("results.total_failure_vm_slices", result.totalFailureVmSlices),
- Updates.set("results.total_vms_submitted", result.totalVmsSubmitted),
- Updates.set("results.total_vms_queued", result.totalVmsQueued),
- Updates.set("results.total_vms_finished", result.totalVmsFinished),
- Updates.set("results.total_vms_failed", result.totalVmsFailed)
+ Updates.set("results.total_requested_burst", results.map { it.totalRequestedBurst }),
+ Updates.set("results.total_granted_burst", results.map { it.totalGrantedBurst }),
+ Updates.set("results.total_overcommitted_burst", results.map { it.totalOvercommittedBurst }),
+ Updates.set("results.total_interfered_burst", results.map { it.totalInterferedBurst }),
+ Updates.set("results.mean_cpu_usage", results.map { it.meanCpuUsage }),
+ Updates.set("results.mean_cpu_demand", results.map { it.meanCpuDemand }),
+ Updates.set("results.mean_num_deployed_images", results.map { it.meanNumDeployedImages }),
+ Updates.set("results.max_num_deployed_images", results.map { it.maxNumDeployedImages }),
+ Updates.set("results.total_power_draw", results.map { it.totalPowerDraw }),
+ Updates.set("results.total_failure_slices", results.map { it.totalFailureSlices }),
+ Updates.set("results.total_failure_vm_slices", results.map { it.totalFailureVmSlices }),
+ Updates.set("results.total_vms_submitted", results.map { it.totalVmsSubmitted }),
+ Updates.set("results.total_vms_queued", results.map { it.totalVmsQueued }),
+ Updates.set("results.total_vms_finished", results.map { it.totalVmsFinished }),
+ Updates.set("results.total_vms_failed", results.map { it.totalVmsFailed })
)
)
}
diff --git a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/TopologyParser.kt b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/TopologyParser.kt
index 5e483271..80bd20f7 100644
--- a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/TopologyParser.kt
+++ b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/TopologyParser.kt
@@ -31,6 +31,7 @@ import com.mongodb.client.model.Projections
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.bson.Document
+import org.bson.types.ObjectId
import org.opendc.compute.core.metal.NODE_CLUSTER
import org.opendc.compute.core.metal.service.ProvisioningService
import org.opendc.compute.core.metal.service.SimpleProvisioningService
@@ -51,7 +52,7 @@ import java.util.*
/**
* A helper class that converts the MongoDB topology into an OpenDC environment.
*/
-public class TopologyParser(private val collection: MongoCollection<Document>, private val id: String) : EnvironmentReader {
+public class TopologyParser(private val collection: MongoCollection<Document>, private val id: ObjectId) : EnvironmentReader {
/**
* Parse the topology with the specified [id].
*/
@@ -60,7 +61,7 @@ public class TopologyParser(private val collection: MongoCollection<Document>, p
val random = Random(0)
for (machine in fetchMachines(id)) {
- val clusterId = machine.getString("rack_id")
+ val clusterId = machine.get("rack_id").toString()
val position = machine.getInteger("position")
val processors = machine.getList("cpus", Document::class.java).flatMap { cpu ->
@@ -121,7 +122,7 @@ public class TopologyParser(private val collection: MongoCollection<Document>, p
/**
* Fetch the metadata of the topology.
*/
- private fun fetchName(id: String): String {
+ private fun fetchName(id: ObjectId): String {
return collection.aggregate(
listOf(
Aggregates.match(Filters.eq("_id", id)),
@@ -135,7 +136,7 @@ public class TopologyParser(private val collection: MongoCollection<Document>, p
/**
* Fetch a topology from the database with the specified [id].
*/
- private fun fetchMachines(id: String): AggregateIterable<Document> {
+ private fun fetchMachines(id: ObjectId): AggregateIterable<Document> {
return collection.aggregate(
listOf(
Aggregates.match(Filters.eq("_id", id)),
diff --git a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/WebExperimentMonitor.kt b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/WebExperimentMonitor.kt
new file mode 100644
index 00000000..7ef25552
--- /dev/null
+++ b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/WebExperimentMonitor.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2020 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.runner.web
+
+import mu.KotlinLogging
+import org.opendc.compute.core.Server
+import org.opendc.compute.core.ServerState
+import org.opendc.compute.core.virt.driver.VirtDriver
+import org.opendc.compute.core.virt.service.VirtProvisioningEvent
+import org.opendc.experiments.sc20.experiment.monitor.ExperimentMonitor
+import org.opendc.experiments.sc20.telemetry.HostEvent
+import kotlin.math.max
+
+/**
+ * An [ExperimentMonitor] that tracks the aggregate metrics for each repeat.
+ */
+public class WebExperimentMonitor : ExperimentMonitor {
+ private val logger = KotlinLogging.logger {}
+ private val currentHostEvent = mutableMapOf<Server, HostEvent>()
+ private var startTime = -1L
+
+ override fun reportVmStateChange(time: Long, server: Server) {
+ if (startTime < 0) {
+ startTime = time
+
+ // Update timestamp of initial event
+ currentHostEvent.replaceAll { _, v -> v.copy(timestamp = startTime) }
+ }
+ }
+
+ override fun reportHostStateChange(
+ time: Long,
+ driver: VirtDriver,
+ server: Server
+ ) {
+ logger.debug { "Host ${server.uid} changed state ${server.state} [$time]" }
+
+ val previousEvent = currentHostEvent[server]
+
+ val roundedTime = previousEvent?.let {
+ val duration = time - it.timestamp
+ val k = 5 * 60 * 1000L // 5 min in ms
+ val rem = duration % k
+
+ if (rem == 0L) {
+ time
+ } else {
+ it.timestamp + duration + k - rem
+ }
+ } ?: time
+
+ reportHostSlice(
+ roundedTime,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0.0,
+ 0.0,
+ 0,
+ server
+ )
+ }
+
+ private val lastPowerConsumption = mutableMapOf<Server, Double>()
+
+ override fun reportPowerConsumption(host: Server, draw: Double) {
+ lastPowerConsumption[host] = draw
+ }
+
+ override fun reportHostSlice(
+ time: Long,
+ requestedBurst: Long,
+ grantedBurst: Long,
+ overcommissionedBurst: Long,
+ interferedBurst: Long,
+ cpuUsage: Double,
+ cpuDemand: Double,
+ numberOfDeployedImages: Int,
+ hostServer: Server,
+ duration: Long
+ ) {
+ val previousEvent = currentHostEvent[hostServer]
+ when {
+ previousEvent == null -> {
+ val event = HostEvent(
+ time,
+ 5 * 60 * 1000L,
+ hostServer,
+ numberOfDeployedImages,
+ requestedBurst,
+ grantedBurst,
+ overcommissionedBurst,
+ interferedBurst,
+ cpuUsage,
+ cpuDemand,
+ lastPowerConsumption[hostServer] ?: 200.0,
+ hostServer.flavor.cpuCount
+ )
+
+ currentHostEvent[hostServer] = event
+ }
+ previousEvent.timestamp == time -> {
+ val event = HostEvent(
+ time,
+ previousEvent.duration,
+ hostServer,
+ numberOfDeployedImages,
+ requestedBurst,
+ grantedBurst,
+ overcommissionedBurst,
+ interferedBurst,
+ cpuUsage,
+ cpuDemand,
+ lastPowerConsumption[hostServer] ?: 200.0,
+ hostServer.flavor.cpuCount
+ )
+
+ currentHostEvent[hostServer] = event
+ }
+ else -> {
+ processHostEvent(previousEvent)
+
+ val event = HostEvent(
+ time,
+ time - previousEvent.timestamp,
+ hostServer,
+ numberOfDeployedImages,
+ requestedBurst,
+ grantedBurst,
+ overcommissionedBurst,
+ interferedBurst,
+ cpuUsage,
+ cpuDemand,
+ lastPowerConsumption[hostServer] ?: 200.0,
+ hostServer.flavor.cpuCount
+ )
+
+ currentHostEvent[hostServer] = event
+ }
+ }
+ }
+
+ private var hostAggregateMetrics: AggregateHostMetrics = AggregateHostMetrics()
+ private val hostMetrics: MutableMap<Server, HostMetrics> = mutableMapOf()
+
+ private fun processHostEvent(event: HostEvent) {
+ val slices = event.duration / SLICE_LENGTH
+
+ hostAggregateMetrics = AggregateHostMetrics(
+ hostAggregateMetrics.totalRequestedBurst + event.requestedBurst,
+ hostAggregateMetrics.totalGrantedBurst + event.grantedBurst,
+ hostAggregateMetrics.totalOvercommittedBurst + event.overcommissionedBurst,
+ hostAggregateMetrics.totalInterferedBurst + event.interferedBurst,
+ hostAggregateMetrics.totalPowerDraw + (slices * (event.powerDraw / 12)),
+ hostAggregateMetrics.totalFailureSlices + if (event.host.state != ServerState.ACTIVE) slices.toLong() else 0,
+ hostAggregateMetrics.totalFailureVmSlices + if (event.host.state != ServerState.ACTIVE) event.vmCount * slices.toLong() else 0
+ )
+
+ hostMetrics.compute(event.host) { key, prev ->
+ HostMetrics(
+ (event.cpuUsage.takeIf { event.host.state == ServerState.ACTIVE } ?: 0.0) + (prev?.cpuUsage ?: 0.0),
+ (event.cpuDemand.takeIf { event.host.state == ServerState.ACTIVE } ?: 0.0) + (prev?.cpuDemand ?: 0.0),
+ event.vmCount + (prev?.vmCount ?: 0),
+ 1 + (prev?.count ?: 0)
+ )
+ }
+ }
+
+ private val SLICE_LENGTH: Long = 5 * 60 * 1000
+
+ public data class AggregateHostMetrics(
+ val totalRequestedBurst: Long = 0,
+ val totalGrantedBurst: Long = 0,
+ val totalOvercommittedBurst: Long = 0,
+ val totalInterferedBurst: Long = 0,
+ val totalPowerDraw: Double = 0.0,
+ val totalFailureSlices: Long = 0,
+ val totalFailureVmSlices: Long = 0,
+ )
+
+ public data class HostMetrics(
+ val cpuUsage: Double,
+ val cpuDemand: Double,
+ val vmCount: Long,
+ val count: Long
+ )
+
+ private var provisionerMetrics: AggregateProvisionerMetrics = AggregateProvisionerMetrics()
+
+ override fun reportProvisionerMetrics(time: Long, event: VirtProvisioningEvent.MetricsAvailable) {
+ provisionerMetrics = AggregateProvisionerMetrics(
+ max(event.totalVmCount, provisionerMetrics.vmTotalCount),
+ max(event.waitingVmCount, provisionerMetrics.vmWaitingCount),
+ max(event.activeVmCount, provisionerMetrics.vmActiveCount),
+ max(event.inactiveVmCount, provisionerMetrics.vmInactiveCount),
+ max(event.failedVmCount, provisionerMetrics.vmFailedCount),
+ )
+ }
+
+ public data class AggregateProvisionerMetrics(
+ val vmTotalCount: Int = 0,
+ val vmWaitingCount: Int = 0,
+ val vmActiveCount: Int = 0,
+ val vmInactiveCount: Int = 0,
+ val vmFailedCount: Int = 0
+ )
+
+ override fun close() {
+ for ((_, event) in currentHostEvent) {
+ processHostEvent(event)
+ }
+ currentHostEvent.clear()
+ }
+
+ public fun getResult(): Result {
+ return Result(
+ hostAggregateMetrics.totalRequestedBurst,
+ hostAggregateMetrics.totalGrantedBurst,
+ hostAggregateMetrics.totalOvercommittedBurst,
+ hostAggregateMetrics.totalInterferedBurst,
+ hostMetrics.map { it.value.cpuUsage / it.value.count }.average(),
+ hostMetrics.map { it.value.cpuDemand / it.value.count }.average(),
+ hostMetrics.map { it.value.vmCount.toDouble() / it.value.count }.average(),
+ hostMetrics.map { it.value.vmCount.toDouble() / it.value.count }.maxOrNull() ?: 0.0,
+ hostAggregateMetrics.totalPowerDraw,
+ hostAggregateMetrics.totalFailureSlices,
+ hostAggregateMetrics.totalFailureVmSlices,
+ provisionerMetrics.vmTotalCount,
+ provisionerMetrics.vmWaitingCount,
+ provisionerMetrics.vmInactiveCount,
+ provisionerMetrics.vmFailedCount,
+ )
+ }
+
+ public data class Result(
+ public val totalRequestedBurst: Long,
+ public val totalGrantedBurst: Long,
+ public val totalOvercommittedBurst: Long,
+ public val totalInterferedBurst: Long,
+ public val meanCpuUsage: Double,
+ public val meanCpuDemand: Double,
+ public val meanNumDeployedImages: Double,
+ public val maxNumDeployedImages: Double,
+ public val totalPowerDraw: Double,
+ public val totalFailureSlices: Long,
+ public val totalFailureVmSlices: Long,
+ public val totalVmsSubmitted: Int,
+ public val totalVmsQueued: Int,
+ public val totalVmsFinished: Int,
+ public val totalVmsFailed: Int
+ )
+}
diff --git a/simulator/opendc-runner-web/src/main/resources/log4j2.xml b/simulator/opendc-runner-web/src/main/resources/log4j2.xml
index 16cedf34..87179332 100644
--- a/simulator/opendc-runner-web/src/main/resources/log4j2.xml
+++ b/simulator/opendc-runner-web/src/main/resources/log4j2.xml
@@ -36,12 +36,6 @@
<Logger name="org.opendc.runner" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
- <Logger name="org.apache.hadoop" level="warn" additivity="false">
- <AppenderRef ref="Console"/>
- </Logger>
- <Logger name="org.apache.spark" level="info" additivity="false">
- <AppenderRef ref="Console"/>
- </Logger>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>