[pbs-devel] [RFC proxmox-backup] create prune simulator
Fabian Ebner
f.ebner at proxmox.com
Thu Sep 17 14:09:19 CEST 2020
A stand-alone ExtJS app that allows experimenting with
different backup schedules and prune parameters.
For performance reasons, the week table does not use
subcomponents, but raw HTML.
Signed-off-by: Fabian Ebner <f.ebner at proxmox.com>
---
Not sure where in our repos the best place to put this is.
I first tried having a panel for each day, each with a
filtered ChainedStore, but the performance was terrible (a few
seconds delay) even with only 10 weeks. The single panel
approach can handle a lot more.
Still missing/ideas:
* make interface prettier
* colors were just added quickly as a POC,
don't know if background-coloring is the best idea
* for the schedule, add a submit button instead of
continuously updating the view
* allow configuring number of weeks
docs/prune-simulator/extjs | 1 +
docs/prune-simulator/index.html | 13 +
docs/prune-simulator/prune-simulator.js | 500 ++++++++++++++++++++++++
3 files changed, 514 insertions(+)
create mode 120000 docs/prune-simulator/extjs
create mode 100644 docs/prune-simulator/index.html
create mode 100644 docs/prune-simulator/prune-simulator.js
diff --git a/docs/prune-simulator/extjs b/docs/prune-simulator/extjs
new file mode 120000
index 00000000..b71ec6ef
--- /dev/null
+++ b/docs/prune-simulator/extjs
@@ -0,0 +1 @@
+/usr/share/javascript/extjs
\ No newline at end of file
diff --git a/docs/prune-simulator/index.html b/docs/prune-simulator/index.html
new file mode 100644
index 00000000..deb26764
--- /dev/null
+++ b/docs/prune-simulator/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+ <title>Prune Backups Simulator</title>
+
+ <link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
+ <script type="text/javascript" src="extjs/ext-all.js"></script>
+ <script type="text/javascript" src="prune-simulator.js"></script>
+</head>
+<body></body>
+</html>
diff --git a/docs/prune-simulator/prune-simulator.js b/docs/prune-simulator/prune-simulator.js
new file mode 100644
index 00000000..53995930
--- /dev/null
+++ b/docs/prune-simulator/prune-simulator.js
@@ -0,0 +1,500 @@
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+ var console = {
+ dir: function() {},
+ log: function() {}
+ };
+}
+
+Ext.onReady(function() {
+
+ // TODO: allow configuring some of these from within the UI?
+ let NUMBER_OF_WEEKS = 15;
+ let NOW = new Date();
+ let COLORS = {
+ 'keep-last': 'green',
+ 'keep-hourly': 'orange',
+ 'keep-daily': 'yellow',
+ 'keep-weekly': 'red',
+ 'keep-monthly': 'blue',
+ 'keep-yearly': 'purple',
+ };
+ let TEXT_COLORS = {
+ 'keep-last': 'white',
+ 'keep-hourly': 'black',
+ 'keep-daily': 'black',
+ 'keep-weekly': 'white',
+ 'keep-monthly': 'white',
+ 'keep-yearly': 'white',
+ };
+
+ Ext.define('PBS.prunesimulator.CalendarEvent', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.prunesimulatorCalendarEvent',
+
+ editable: true,
+
+ displayField: 'text',
+ valueField: 'value',
+ queryMode: 'local',
+
+ store: {
+ field: [ 'value', 'text'],
+ data: [
+ { value: '0/2:00', text: "Every two hours" },
+ { value: '0/6:00', text: "Every six hours" },
+ { value: '2,22:30', text: "At 02:30 and 22:30" },
+ ],
+ },
+
+ tpl: [
+ '<ul class="x-list-plain"><tpl for=".">',
+ '<li role="option" class="x-boundlist-item">{text}</li>',
+ '</tpl></ul>',
+ ],
+
+ displayTpl: [
+ '<tpl for=".">',
+ '{value}',
+ '</tpl>',
+ ],
+ });
+
+ Ext.define('PBS.prunesimulator.DayOfWeekSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.prunesimulatorDayOfWeekSelector',
+
+ displayField: 'text',
+ valueField: 'value',
+ queryMode: 'local',
+
+ store: {
+ field: ['value', 'text'],
+ data: [
+ { value: 'mon', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[1]) },
+ { value: 'tue', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[2]) },
+ { value: 'wed', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[3]) },
+ { value: 'thu', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[4]) },
+ { value: 'fri', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[5]) },
+ { value: 'sat', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[6]) },
+ { value: 'sun', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[0]) },
+ ],
+ },
+ });
+
+ Ext.define('pbs-prune-list', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'backuptime',
+ type: 'date',
+ dateFormat: 'timestamp',
+ },
+ ]
+ });
+
+ Ext.define('PBS.prunesimulator.WeekTable', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.prunesimulatorWeekTable',
+
+ autoScroll: true,
+ height: 800,
+
+ reload: function() {
+ let me = this;
+ let backups = me.store.data.items;
+
+ let html = '<table>';
+
+ let now = new Date(NOW.getTime());
+ let skip = 7 - parseInt(Ext.Date.format(now, 'N'));
+ let tableStartDate = Ext.Date.add(now, Ext.Date.DAY, skip);
+
+ let bIndex = 0;
+
+ for (i = 0; i < NUMBER_OF_WEEKS; i++) {
+ html += '<tr>';
+
+ for (j = 0; j < 7; j++) {
+ html += '<td style="vertical-align: top;' +
+ 'width: 150px;' +
+ 'border:#000000 1px solid;' +
+ '">';
+
+ let date = Ext.Date.subtract(tableStartDate, Ext.Date.DAY, j + 7 * i);
+ let currentDay = Ext.Date.format(date, 'd/m/Y');
+
+ let backup = backups[bIndex];
+
+ let isBackupOnDay = function(backup, day) {
+ return backup && Ext.Date.format(backup.data.backuptime, 'd/m/Y') === day;
+ };
+
+ html += '<table><tr><th>' + Ext.Date.format(date, 'D, d M Y') + '</th>';
+
+ while (isBackupOnDay(backup, currentDay)) {
+ html += '<tr><td>';
+
+ let text = Ext.Date.format(backup.data.backuptime, 'H:i');
+ if (backup.data.mark === 'remove') {
+ html += '<div style="text-decoration: line-through; ' +
+ 'font-weight: bold;">' + text + '</div>';
+ } else {
+ let bgColor = COLORS[backup.data.keepName];
+ let textColor = TEXT_COLORS[backup.data.keepName];
+ html += '<div style="background-color: ' + bgColor + '; ' +
+ 'color: ' + textColor + ';">' + text + '</div>';
+ }
+ html += '</td></tr>';
+ backup = backups[++bIndex];
+ }
+ html += '</table>';
+ html += '</div>';
+ html += '</td>';
+ }
+
+ html += '</tr>';
+ }
+
+ me.setHtml(html);
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.store) {
+ throw "no store specified";
+ }
+
+ let reload = function() {
+ me.reload();
+ };
+
+ me.store.on("datachanged", reload);
+
+ me.callParent();
+
+ me.reload();
+ },
+ });
+
+ Ext.define('PBS.PruneSimulatorPanel', {
+ extend: 'Ext.panel.Panel',
+
+ getValues: function() {
+ let me = this;
+
+ let values = {};
+
+ Ext.Array.each(me.query('[isFormField]'), function(field) {
+ let data = field.getSubmitData();
+ Ext.Object.each(data, function(name, val) {
+ values[name] = val;
+ });
+ });
+
+ return values;
+ },
+
+ layout: {
+ type: 'table',
+ columns: 2,
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ this.reload(); // initial load
+ },
+
+ control: {
+ field: { change: 'reload' }
+ },
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+
+ let params = view.getValues();
+
+ //TODO check format for schedule
+
+ let [ startTimesStr, interval ] = params['schedule-time'].split('/');
+
+ if (interval) {
+ let [ intervalBig, intervalSmall ] = interval.split(':');
+ if (intervalSmall) {
+ interval = parseInt(intervalBig) * 60 + parseInt(intervalSmall);
+ } else {
+ interval = parseInt(intervalBig); // no ':' means only minutes
+ }
+ }
+
+ let [ startTimesHours, startTimesMinute ] = startTimesStr.split(':');
+ let startTimesHoursArray = startTimesHours.split(',');
+
+ let startTimes = [];
+ startTimesHoursArray.forEach(function(hour) {
+ startTimesMinute = startTimesMinute || '0';
+ startTimes.push([hour, startTimesMinute]);
+ });
+
+
+ let backups = me.populateFromSchedule(params['schedule-weekdays'], startTimes, interval);
+ me.pruneSelect(backups, params);
+
+ view.pruneStore.setData(backups);
+ },
+
+ // backups are sorted descending by date
+ populateFromSchedule: function(weekdays, startTimes, interval) {
+
+ interval = interval || 60 * 60 * 24;
+
+ let weekdayFlags = [
+ weekdays.includes('sun'),
+ weekdays.includes('mon'),
+ weekdays.includes('tue'),
+ weekdays.includes('wed'),
+ weekdays.includes('thu'),
+ weekdays.includes('fri'),
+ weekdays.includes('sat'),
+ ];
+
+ let baseDate = new Date(NOW.getTime());
+
+ let backups = [];
+
+ startTimes.forEach(function(startTime) {
+ baseDate.setHours(startTime[0]);
+ baseDate.setMinutes(startTime[1]);
+ for (i = 0; i < 7 * NUMBER_OF_WEEKS; i++) {
+ let date = Ext.Date.subtract(baseDate, Ext.Date.DAY, i);
+ let weekday = parseInt(Ext.Date.format(date, 'w'));
+ if (weekdayFlags[weekday]) {
+ let backupsToday = [];
+ while (parseInt(Ext.Date.format(date, 'w')) === weekday) {
+ backupsToday.push({
+ backuptime: date,
+ });
+ date = Ext.Date.add(date, Ext.Date.MINUTE, interval);
+ }
+ while (backupsToday.length > 0) {
+ backups.push(backupsToday.pop());
+ }
+ }
+ }
+ });
+
+ return backups;
+ },
+
+ pruneMark: function(backups, keepCount, keepName, idFunc) {
+ if (!keepCount) {
+ return;
+ }
+
+ let alreadyIncluded = {};
+ let newlyIncluded = {};
+ let newlyIncludedCount = 0;
+
+ let finished = false;
+
+ backups.forEach(function(backup) {
+ let mark = backup.mark;
+ let id = idFunc(backup);
+
+ if (finished || alreadyIncluded[id]) {
+ return;
+ }
+
+ if (mark) {
+ if (mark === 'keep') {
+ alreadyIncluded[id] = true;
+ }
+ return;
+ }
+
+ if (!newlyIncluded[id]) {
+ if (newlyIncludedCount >= keepCount) {
+ finished = true;
+ return;
+ }
+ newlyIncluded[id] = true;
+ newlyIncludedCount++;
+ backup.mark = 'keep';
+ backup.keepName = keepName;
+ } else {
+ backup.mark = 'remove';
+ }
+ });
+ },
+
+ // backups need to be sorted descending by date
+ pruneSelect: function(backups, keepParams) {
+ let me = this;
+
+ me.pruneMark(backups, keepParams['keep-last'], 'keep-last', function(backup) {
+ return backup.backuptime;
+ });
+ me.pruneMark(backups, keepParams['keep-hourly'], 'keep-hourly', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'H/d/m/Y');
+ });
+ me.pruneMark(backups, keepParams['keep-daily'], 'keep-daily', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'd/m/Y');
+ });
+ me.pruneMark(backups, keepParams['keep-weekly'], 'keep-weekly', function(backup) {
+ // ISO-8601 week and week-based year
+ return Ext.Date.format(backup.backuptime, 'W/o');
+ });
+ me.pruneMark(backups, keepParams['keep-monthly'], 'keep-monthly', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'm/Y');
+ });
+ me.pruneMark(backups, keepParams['keep-yearly'], 'keep-yearly', function(backup) {
+ return Ext.Date.format(backup.backuptime, 'Y');
+ });
+
+ backups.forEach(function(backup) {
+ backup.mark = backup.mark || 'remove';
+ });
+ },
+ },
+
+ keepItems: [
+ {
+ xtype: 'numberfield',
+ name: 'keep-last',
+ allowBlank: true,
+ fieldLabel: 'keep-last',
+ minValue: 0,
+ value: 5,
+ fieldStyle: 'background-color: ' + COLORS['keep-last'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-last'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-hourly',
+ allowBlank: true,
+ fieldLabel: 'keep-hourly',
+ minValue: 0,
+ value: 4,
+ fieldStyle: 'background-color: ' + COLORS['keep-hourly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-hourly'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-daily',
+ allowBlank: true,
+ fieldLabel: 'keep-daily',
+ minValue: 0,
+ value: 3,
+ fieldStyle: 'background-color: ' + COLORS['keep-daily'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-daily'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-weekly',
+ allowBlank: true,
+ fieldLabel: 'keep-weekly',
+ minValue: 0,
+ value: 2,
+ fieldStyle: 'background-color: ' + COLORS['keep-weekly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-weekly'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-monthly',
+ allowBlank: true,
+ fieldLabel: 'keep-monthly',
+ minValue: 0,
+ value: 1,
+ fieldStyle: 'background-color: ' + COLORS['keep-monthly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-monthly'] + ';',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'keep-yearly',
+ allowBlank: true,
+ fieldLabel: 'keep-yearly',
+ minValue: 0,
+ value: 0,
+ fieldStyle: 'background-color: ' + COLORS['keep-yearly'] + '; ' +
+ 'color: ' + TEXT_COLORS['keep-yearly'] + ';',
+ }
+ ],
+
+ scheduleItems: [
+ {
+ xtype: 'prunesimulatorDayOfWeekSelector',
+ name: 'schedule-weekdays',
+ fieldLabel: 'Day of week',
+ value: ['mon','tue','wed','thu','fri','sat','sun'],
+ allowBlank: false,
+ multiSelect: true,
+ },
+ {
+ xtype: 'prunesimulatorCalendarEvent',
+ name: 'schedule-time',
+ allowBlank: false,
+ value: '0/6:00',
+ fieldLabel: 'Backup schedule',
+ },
+/* TODO add button and only update schedule when clicked
+ {
+ xtype: 'button',
+ name: 'schedule-button',
+ text: 'Update Schedule',
+ },
+ */
+ ],
+
+ initComponent : function() {
+ var me = this;
+
+ me.pruneStore = Ext.create('Ext.data.Store', {
+ model: 'pbs-prune-list',
+ sorters: { property: 'backuptime', direction: 'DESC' },
+ });
+
+ me.column2 = [
+ {
+ xtype: 'prunesimulatorWeekTable',
+ store: me.pruneStore,
+ },
+ ];
+
+ me.items = [
+ {
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: me.keepItems,
+ },
+ {
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: me.scheduleItems,
+ },
+ {
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: me.column2,
+ colspan: 2,
+ },
+ ];
+
+ me.callParent();
+ }
+ });
+
+ let simulator = Ext.create('PBS.PruneSimulatorPanel', {});
+
+ Ext.create('Ext.container.Viewport', {
+ layout: 'border',
+ renderTo: Ext.getBody(),
+ items: [
+ simulator,
+ ],
+ });
+});
+
--
2.20.1
More information about the pbs-devel
mailing list