diff options
| author | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-01-24 12:06:09 +0100 |
|---|---|---|
| committer | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-01-24 12:06:09 +0100 |
| commit | c96e6ffafb62bde1e08987b1fdf3c0786487f6ec (patch) | |
| tree | 37eaf4cf199ca77dc131b4212c526b707adf2e30 /src/scripts/controllers/simulation | |
Initial commit
Diffstat (limited to 'src/scripts/controllers/simulation')
| -rw-r--r-- | src/scripts/controllers/simulation/chart.ts | 241 | ||||
| -rw-r--r-- | src/scripts/controllers/simulation/statecache.ts | 205 | ||||
| -rw-r--r-- | src/scripts/controllers/simulation/taskview.ts | 64 | ||||
| -rw-r--r-- | src/scripts/controllers/simulation/timeline.ts | 161 |
4 files changed, 671 insertions, 0 deletions
diff --git a/src/scripts/controllers/simulation/chart.ts b/src/scripts/controllers/simulation/chart.ts new file mode 100644 index 00000000..84009622 --- /dev/null +++ b/src/scripts/controllers/simulation/chart.ts @@ -0,0 +1,241 @@ +import * as c3 from "c3"; +import {InteractionLevel, MapController} from "../mapcontroller"; +import {ColorRepresentation, SimulationController} from "../simulationcontroller"; +import {Util} from "../../util"; + + +export interface IStateColumn { + loadFractions: string[] | number[]; + inUseMemoryMb: string[] | number[]; + temperatureC: string[] | number[]; +} + + +export class ChartController { + public roomSeries: { [key: number]: IStateColumn }; + public rackSeries: { [key: number]: IStateColumn }; + public machineSeries: { [key: number]: IStateColumn }; + public chart: c3.ChartAPI; + public machineChart: c3.ChartAPI; + + private simulationController: SimulationController; + private mapController: MapController; + private chartData: (string | number)[][]; + private xSeries: (string | number)[]; + private names: { [key: string]: string }; + + + constructor(simulationController: SimulationController) { + this.simulationController = simulationController; + this.mapController = simulationController.mapController; + } + + public setup(): void { + this.names = {}; + + this.roomSeries = {}; + this.rackSeries = {}; + this.machineSeries = {}; + + this.simulationController.sections.forEach((simulationSection: ISection) => { + simulationSection.datacenter.rooms.forEach((room: IRoom) => { + if (room.roomType === "SERVER" && this.roomSeries[room.id] === undefined) { + this.names["ro" + room.id] = (room.name === "" || room.name === undefined) ? + "Unnamed room" : room.name; + + this.roomSeries[room.id] = { + loadFractions: ["ro" + room.id], + inUseMemoryMb: ["ro" + room.id], + temperatureC: ["ro" + room.id] + }; + } + + room.tiles.forEach((tile: ITile) => { + if (tile.object !== undefined && tile.objectType === "RACK" && this.rackSeries[tile.objectId] === undefined) { + let objectName = (<IRack>tile.object).name; + this.names["ra" + tile.objectId] = objectName === "" || objectName === undefined ? + "Unnamed rack" : objectName; + + this.rackSeries[tile.objectId] = { + loadFractions: ["ra" + tile.objectId], + inUseMemoryMb: ["ra" + tile.objectId], + temperatureC: ["ra" + tile.objectId] + }; + + (<IRack>tile.object).machines.forEach((machine: IMachine) => { + if (machine === null || this.machineSeries[machine.id] !== undefined) { + return; + } + + this.names["ma" + machine.id] = "Machine at position " + (machine.position + 1).toString(); + + this.machineSeries[machine.id] = { + loadFractions: ["ma" + machine.id], + inUseMemoryMb: ["ma" + machine.id], + temperatureC: ["ma" + machine.id] + }; + }); + } + }); + }); + }); + + + this.xSeries = ["time"]; + this.chartData = [this.xSeries]; + + this.chart = this.chartSetup("#statistics-chart"); + this.machineChart = this.chartSetup("#machine-statistics-chart"); + } + + public chartSetup(chartId: string): c3.ChartAPI { + return c3.generate({ + bindto: chartId, + data: { + xFormat: '%S', + x: "time", + columns: this.chartData, + names: this.names + }, + axis: { + x: { + type: "timeseries", + tick: { + format: function (time: Date) { + let formattedTime = time.getSeconds() + "s"; + + if (time.getMinutes() > 0) { + formattedTime = time.getMinutes() + "m" + formattedTime; + } + if (time.getHours() > 0) { + formattedTime = time.getHours() + "h" + formattedTime; + } + + return formattedTime; + }, + culling: { + max: 5 + }, + count: 8 + }, + padding: { + left: 0, + right: 10 + } + }, + y: { + min: 0, + max: 1, + padding: { + top: 0, + bottom: 0 + }, + tick: { + format: function (d) { + return (Math.round(d * 100) / 100).toString(); + } + } + } + } + }); + } + + public update(): void { + this.xSeries = (<(number|string)[]>["time"]).concat(Util.range(this.simulationController.currentTick)); + + this.chartData = [this.xSeries]; + + let prefix = ""; + let machineId = -1; + if (this.mapController.interactionLevel === InteractionLevel.BUILDING) { + for (let roomId in this.roomSeries) { + if (this.roomSeries.hasOwnProperty(roomId)) { + if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) { + this.chartData.push(this.roomSeries[roomId].loadFractions); + } + } + } + prefix = "ro"; + } else if (this.mapController.interactionLevel === InteractionLevel.ROOM) { + for (let rackId in this.rackSeries) { + if (this.rackSeries.hasOwnProperty(rackId) && + this.simulationController.rackToRoomMap[rackId] === + this.mapController.roomModeController.currentRoom.id) { + if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) { + this.chartData.push(this.rackSeries[rackId].loadFractions); + } + } + } + prefix = "ra"; + } else if (this.mapController.interactionLevel === InteractionLevel.NODE) { + if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) { + this.chartData.push( + this.machineSeries[this.mapController.nodeModeController.currentMachine.id].loadFractions + ); + } + prefix = "ma"; + machineId = this.mapController.nodeModeController.currentMachine.id; + } + + let unloads: string[] = []; + for (let id in this.names) { + if (this.names.hasOwnProperty(id)) { + if (machineId === -1) { + if (id.substr(0, 2) !== prefix || + (this.mapController.interactionLevel === InteractionLevel.ROOM && + this.simulationController.rackToRoomMap[parseInt(id.substr(2))] !== + this.mapController.roomModeController.currentRoom.id)) { + unloads.push(id); + } + } + else { + if (id !== prefix + machineId) { + unloads.push(id); + } + } + } + } + + let targetChart: c3.ChartAPI; + if (this.mapController.interactionLevel === InteractionLevel.NODE) { + targetChart = this.machineChart; + } else { + targetChart = this.chart; + } + + targetChart.load({ + columns: this.chartData, + unload: unloads + }); + + } + + public tickUpdated(tick: number): void { + let roomStates: IRoomState[] = this.simulationController.stateCache.stateList[tick].roomStates; + roomStates.forEach((roomState: IRoomState) => { + ChartController.insertAtIndex(this.roomSeries[roomState.roomId].loadFractions, tick + 1, roomState.loadFraction); + }); + + let rackStates: IRackState[] = this.simulationController.stateCache.stateList[tick].rackStates; + rackStates.forEach((rackState: IRackState) => { + ChartController.insertAtIndex(this.rackSeries[rackState.rackId].loadFractions, tick + 1, rackState.loadFraction); + }); + + let machineStates: IMachineState[] = this.simulationController.stateCache.stateList[tick].machineStates; + machineStates.forEach((machineState: IMachineState) => { + ChartController.insertAtIndex(this.machineSeries[machineState.machineId].loadFractions, tick + 1, machineState.loadFraction); + }); + } + + private static insertAtIndex(list: any[], index: number, data: any): void { + if (index > list.length) { + let i = list.length; + while (i < index) { + list[i] = null; + i++; + } + } + + list[index] = data; + } +}
\ No newline at end of file diff --git a/src/scripts/controllers/simulation/statecache.ts b/src/scripts/controllers/simulation/statecache.ts new file mode 100644 index 00000000..32f8f4e4 --- /dev/null +++ b/src/scripts/controllers/simulation/statecache.ts @@ -0,0 +1,205 @@ +import {SimulationController} from "../simulationcontroller"; + + +export class StateCache { + public static CACHE_INTERVAL = 3000; + private static PREFERRED_CACHE_ADVANCE = 5; + + public stateList: {[key: number]: ITickState}; + public lastCachedTick: number; + public cacheBlock: boolean; + + private simulationController: SimulationController; + private intervalId: number; + private caching: boolean; + + // Item caches + private machineCache: {[keys: number]: IMachine}; + private rackCache: {[keys: number]: IRack}; + private roomCache: {[keys: number]: IRoom}; + private taskCache: {[keys: number]: ITask}; + + + constructor(simulationController: SimulationController) { + this.stateList = {}; + this.lastCachedTick = -1; + this.cacheBlock = true; + this.simulationController = simulationController; + this.caching = false; + } + + public startCaching(): void { + this.machineCache = {}; + this.rackCache = {}; + this.roomCache = {}; + this.taskCache = {}; + + this.simulationController.mapView.currentDatacenter.rooms.forEach((room: IRoom) => { + this.addRoomToCache(room); + }); + this.simulationController.currentExperiment.trace.tasks.forEach((task: ITask) => { + this.taskCache[task.id] = task; + }); + + this.caching = true; + + this.cache(); + this.intervalId = setInterval(() => { + this.cache(); + }, StateCache.CACHE_INTERVAL); + } + + private addRoomToCache(room: IRoom) { + this.roomCache[room.id] = room; + + room.tiles.forEach((tile: ITile) => { + if (tile.objectType === "RACK") { + this.rackCache[tile.objectId] = <IRack>tile.object; + + (<IRack> tile.object).machines.forEach((machine: IMachine) => { + if (machine !== null) { + this.machineCache[machine.id] = machine; + } + }); + } + }); + } + + public stopCaching(): void { + if (this.caching) { + this.caching = false; + clearInterval(this.intervalId); + } + } + + private cache(): void { + let tick = this.lastCachedTick + 1; + + this.updateLastTick().then(() => { + // Check if end of simulated region has been reached + if (this.lastCachedTick > this.simulationController.lastSimulatedTick) { + return; + } + + this.fetchAllStatesOfTick(tick).then((data: ITickState) => { + this.stateList[tick] = data; + + this.updateTasks(tick); + + // Update chart cache + this.simulationController.chartController.tickUpdated(tick); + + this.lastCachedTick++; + + if (!this.cacheBlock && this.lastCachedTick - this.simulationController.currentTick <= 0) { + this.cacheBlock = true; + return; + } + + if (this.cacheBlock) { + if (this.lastCachedTick - this.simulationController.currentTick >= StateCache.PREFERRED_CACHE_ADVANCE) { + this.cacheBlock = false; + } + } + }); + }); + } + + private updateTasks(tick: number): void { + const taskIDsInTick = []; + + this.stateList[tick].taskStates.forEach((taskState: ITaskState) => { + taskIDsInTick.push(taskState.taskId); + if (this.stateList[tick - 1] !== undefined) { + let previousFlops = 0; + const previousStates = this.stateList[tick - 1].taskStates; + + for (let i = 0; i < previousStates.length; i++) { + if (previousStates[i].taskId === taskState.taskId) { + previousFlops = previousStates[i].flopsLeft; + break; + } + } + + if (previousFlops > 0 && taskState.flopsLeft === 0) { + taskState.task.finishedTick = tick; + } + } + }); + + // Generate pseudo-task-states for tasks that haven't started yet or have already finished + const traceTasks = this.simulationController.currentExperiment.trace.tasks; + if (taskIDsInTick.length !== traceTasks.length) { + traceTasks + .filter((task: ITask) => { + return taskIDsInTick.indexOf(task.id) === -1; + }) + .forEach((task: ITask) => { + const flopStateCount = task.startTick >= tick ? task.totalFlopCount : 0; + + this.stateList[tick].taskStates.push({ + id: -1, + taskId: task.id, + task: task, + experimentId: this.simulationController.currentExperiment.id, + tick, + flopsLeft: flopStateCount + }); + }); + } + + this.stateList[tick].taskStates.sort((a: ITaskState, b: ITaskState) => { + return a.task.startTick - b.task.startTick; + }); + } + + private updateLastTick(): Promise<void> { + return this.simulationController.mapController.api.getLastSimulatedTickByExperiment( + this.simulationController.simulation.id, this.simulationController.currentExperiment.id).then((data) => { + this.simulationController.lastSimulatedTick = data; + }); + } + + private fetchAllStatesOfTick(tick: number): Promise<ITickState> { + let tickState: ITickState = { + tick, + machineStates: [], + rackStates: [], + roomStates: [], + taskStates: [] + }; + const promises = []; + + promises.push(this.simulationController.mapController.api.getMachineStatesByTick( + this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id, + tick, this.machineCache + ).then((states: IMachineState[]) => { + tickState.machineStates = states; + })); + + promises.push(this.simulationController.mapController.api.getRackStatesByTick( + this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id, + tick, this.rackCache + ).then((states: IRackState[]) => { + tickState.rackStates = states; + })); + + promises.push(this.simulationController.mapController.api.getRoomStatesByTick( + this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id, + tick, this.roomCache + ).then((states: IRoomState[]) => { + tickState.roomStates = states; + })); + + promises.push(this.simulationController.mapController.api.getTaskStatesByTick( + this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id, + tick, this.taskCache + ).then((states: ITaskState[]) => { + tickState.taskStates = states; + })); + + return Promise.all(promises).then(() => { + return tickState; + }); + } +} diff --git a/src/scripts/controllers/simulation/taskview.ts b/src/scripts/controllers/simulation/taskview.ts new file mode 100644 index 00000000..d989e103 --- /dev/null +++ b/src/scripts/controllers/simulation/taskview.ts @@ -0,0 +1,64 @@ +import * as $ from "jquery"; +import {SimulationController} from "../simulationcontroller"; +import {Util} from "../../util"; + + +export class TaskViewController { + private simulationController: SimulationController; + + + constructor(simulationController: SimulationController) { + this.simulationController = simulationController; + } + + /** + * Populates and displays the list of tasks with their current state. + */ + public update() { + const container = $(".task-list"); + container.children().remove(".task-element"); + + this.simulationController.stateCache.stateList[this.simulationController.currentTick].taskStates + .forEach((taskState: ITaskState) => { + const html = this.generateTaskElementHTML(taskState); + container.append(html); + }); + } + + private generateTaskElementHTML(taskState: ITaskState) { + let iconType, timeInfo; + + if (taskState.task.startTick > this.simulationController.currentTick) { + iconType = "glyphicon-time"; + timeInfo = "Not started yet"; + } else if (taskState.task.startTick <= this.simulationController.currentTick && taskState.flopsLeft > 0) { + iconType = "glyphicon-refresh"; + timeInfo = "Started at " + Util.convertSecondsToFormattedTime(taskState.task.startTick); + } else if (taskState.flopsLeft === 0) { + iconType = "glyphicon-ok"; + timeInfo = "Started at " + Util.convertSecondsToFormattedTime(taskState.task.startTick); + } + + // Calculate progression ratio + const progress = 1 - (taskState.flopsLeft / taskState.task.totalFlopCount); + + // Generate completion text + const flopsCompleted = taskState.task.totalFlopCount - taskState.flopsLeft; + const completionInfo = "Completed: " + flopsCompleted + " / " + taskState.task.totalFlopCount + " FLOPS"; + + return '<div class="task-element">' + + ' <div class="task-icon glyphicon ' + iconType + '"></div>' + + ' <div class="task-info">' + + ' <div class="task-time">' + timeInfo + + ' </div>' + + ' <div class="progress">' + + ' <div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' + + progress * 100 + '%"' + + ' aria-valuemin="0" aria-valuemax="100" style="width: ' + progress * 100 + '%">' + + ' </div>' + + ' </div>' + + ' <div class="task-flops">' + completionInfo + '</div>' + + ' </div>' + + '</div>'; + } +} diff --git a/src/scripts/controllers/simulation/timeline.ts b/src/scripts/controllers/simulation/timeline.ts new file mode 100644 index 00000000..a558afe1 --- /dev/null +++ b/src/scripts/controllers/simulation/timeline.ts @@ -0,0 +1,161 @@ +import {SimulationController} from "../simulationcontroller"; +import {Util} from "../../util"; +import * as $ from "jquery"; + + +export class TimelineController { + private simulationController: SimulationController; + private startLabel: JQuery; + private endLabel: JQuery; + private playButton: JQuery; + private loadingIcon: JQuery; + private cacheSection: JQuery; + private timeMarker: JQuery; + private timeline: JQuery; + private timeUnitFraction: number; + private timeMarkerWidth: number; + private timelineWidth: number; + + + constructor(simulationController: SimulationController) { + this.simulationController = simulationController; + this.startLabel = $(".timeline-container .labels .start-time-label"); + this.endLabel = $(".timeline-container .labels .end-time-label"); + this.playButton = $(".timeline-container .play-btn"); + this.loadingIcon = this.playButton.find("img"); + this.cacheSection = $(".timeline-container .timeline .cache-section"); + this.timeMarker = $(".timeline-container .timeline .time-marker"); + this.timeline = $(".timeline-container .timeline"); + this.timeMarkerWidth = this.timeMarker.width(); + this.timelineWidth = this.timeline.width(); + } + + public togglePlayback(): void { + if (this.simulationController.stateCache.cacheBlock) { + this.simulationController.playing = false; + return; + } + this.simulationController.playing = !this.simulationController.playing; + this.setButtonIcon(); + } + + public setupListeners(): void { + this.playButton.on("click", () => { + this.togglePlayback(); + }); + + $(".timeline-container .timeline").on("click", (event: JQueryEventObject) => { + let parentOffset = $(event.target).closest(".timeline").offset(); + let clickX = event.pageX - parentOffset.left; + + let newTick = Math.round(clickX / (this.timelineWidth * this.timeUnitFraction)); + + if (newTick > this.simulationController.stateCache.lastCachedTick) { + newTick = this.simulationController.stateCache.lastCachedTick; + } + this.simulationController.currentTick = newTick; + this.simulationController.checkCurrentSimulationSection(); + this.simulationController.update(); + }); + } + + public setButtonIcon(): void { + if (this.simulationController.playing && !this.playButton.hasClass("glyphicon-pause")) { + this.playButton.removeClass("glyphicon-play").addClass("glyphicon-pause"); + } else if (!this.simulationController.playing && !this.playButton.hasClass("glyphicon-play")) { + this.playButton.removeClass("glyphicon-pause").addClass("glyphicon-play"); + } + } + + public update(): void { + this.timeUnitFraction = 1 / (this.simulationController.lastSimulatedTick + 1); + this.timelineWidth = $(".timeline-container .timeline").width(); + + this.updateTimeLabels(); + + this.cacheSection.css("width", this.calculateTickPosition(this.simulationController.stateCache.lastCachedTick)); + this.timeMarker.css("left", this.calculateTickPosition(this.simulationController.currentTick)); + + this.updateTaskIndicators(); + this.updateSectionMarkers(); + + if (this.simulationController.stateCache.cacheBlock) { + this.playButton.removeClass("glyphicon-pause").removeClass("glyphicon-play"); + this.loadingIcon.show(); + } else { + this.loadingIcon.hide(); + this.setButtonIcon(); + } + } + + private updateTimeLabels(): void { + this.startLabel.text(Util.convertSecondsToFormattedTime(this.simulationController.currentTick)); + this.endLabel.text(Util.convertSecondsToFormattedTime(this.simulationController.lastSimulatedTick)); + } + + private updateSectionMarkers(): void { + $(".section-marker").remove(); + + this.simulationController.sections.forEach((simulationSection: ISection) => { + if (simulationSection.startTick === 0) { + return; + } + + this.timeline.append( + $('<div class="section-marker">') + .css("left", this.calculateTickPosition(simulationSection.startTick)) + ); + }); + } + + private updateTaskIndicators(): void { + $(".task-indicator").remove(); + + let tickStateTypes = { + "queueEntryTick": "task-queued", + "startTick": "task-started", + "finishedTick": "task-finished" + }; + + if (this.simulationController.stateCache.lastCachedTick === -1) { + return; + } + + let indicatorCountList = new Array(this.simulationController.stateCache.lastCachedTick); + let indicator; + this.simulationController.currentExperiment.trace.tasks.forEach((task: ITask) => { + for (let tickStateType in tickStateTypes) { + if (!tickStateTypes.hasOwnProperty(tickStateType)) { + continue; + } + + if (task[tickStateType] !== undefined && + task[tickStateType] <= this.simulationController.stateCache.lastCachedTick) { + + let bottomOffset; + if (indicatorCountList[task[tickStateType]] === undefined) { + indicatorCountList[task[tickStateType]] = 1; + bottomOffset = 0; + } else { + bottomOffset = indicatorCountList[task[tickStateType]] * 10; + indicatorCountList[task[tickStateType]]++; + } + indicator = $('<div class="task-indicator ' + tickStateTypes[tickStateType] + '">') + .css("left", this.calculateTickPosition(task[tickStateType])) + .css("bottom", bottomOffset); + this.timeline.append(indicator); + } + } + }); + } + + private calculateTickPosition(tick: number): string { + let correction = 0; + if (this.timeUnitFraction * this.timelineWidth > this.timeMarkerWidth) { + correction = (this.timeUnitFraction * this.timelineWidth - this.timeMarkerWidth) * + (tick / this.simulationController.lastSimulatedTick); + } + + return (100 * (this.timeUnitFraction * tick + correction / this.timelineWidth)) + "%"; + } +}
\ No newline at end of file |
