summaryrefslogtreecommitdiff
path: root/src/scripts/controllers/simulation
diff options
context:
space:
mode:
authorGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-01-24 12:06:09 +0100
committerGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-01-24 12:06:09 +0100
commitc96e6ffafb62bde1e08987b1fdf3c0786487f6ec (patch)
tree37eaf4cf199ca77dc131b4212c526b707adf2e30 /src/scripts/controllers/simulation
Initial commit
Diffstat (limited to 'src/scripts/controllers/simulation')
-rw-r--r--src/scripts/controllers/simulation/chart.ts241
-rw-r--r--src/scripts/controllers/simulation/statecache.ts205
-rw-r--r--src/scripts/controllers/simulation/taskview.ts64
-rw-r--r--src/scripts/controllers/simulation/timeline.ts161
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