21121: Add a basic test for report generation
[arvados.git] / tools / cluster-activity / test / test_report.html
1 <!doctype html>
2 <html>
3   <head>
4     <title>Cluster report for xzzz1 from 2024-04-04 to 2024-04-06</title>
5
6
7         <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.0.0/dygraph.min.js"></script>
8         <script type="text/javascript">
9         var chartdata = [{"label": "Concurrent running containers", "charts": [{"data": [[new Date("2024-04-06T11:00:00Z"), 3], [new Date("2024-04-06T11:05:00Z"), 5], [new Date("2024-04-06T11:10:00Z"), 2], [new Date("2024-04-06T11:15:00Z"), 5], [new Date("2024-04-06T11:20:00Z"), 3]], "options": {"legend": "always", "connectSeparatedPoints": true, "labels": ["date", "containers"], "includeZero": true, "title": "Concurrent running containers"}}]}, {"label": "Data under management", "charts": [{"data": [[new Date("2024-04-06T11:00:00Z"), 3], [new Date("2024-04-06T11:05:00Z"), 5], [new Date("2024-04-06T11:10:00Z"), 2], [new Date("2024-04-06T11:15:00Z"), 5], [new Date("2024-04-06T11:20:00Z"), 3]], "options": {"legend": "always", "connectSeparatedPoints": true, "labels": ["date", "managed"], "includeZero": true, "title": "Data under management"}}]}, {"label": "Storage usage", "charts": [{"data": [[new Date("2024-04-06T11:00:00Z"), 3], [new Date("2024-04-06T11:05:00Z"), 5], [new Date("2024-04-06T11:10:00Z"), 2], [new Date("2024-04-06T11:15:00Z"), 5], [new Date("2024-04-06T11:20:00Z"), 3]], "options": {"legend": "always", "connectSeparatedPoints": true, "labels": ["date", "used"], "includeZero": true, "title": "Storage usage"}}]}];
10 // Copyright (c) 2009 Dan Vanderkam. All rights reserved.
11 //
12 // SPDX-License-Identifier: MIT
13
14 /**
15  * Synchronize zooming and/or selections between a set of dygraphs.
16  *
17  * Usage:
18  *
19  *   var g1 = new Dygraph(...),
20  *       g2 = new Dygraph(...),
21  *       ...;
22  *   var sync = Dygraph.synchronize(g1, g2, ...);
23  *   // charts are now synchronized
24  *   sync.detach();
25  *   // charts are no longer synchronized
26  *
27  * You can set options using the last parameter, for example:
28  *
29  *   var sync = Dygraph.synchronize(g1, g2, g3, {
30  *      selection: true,
31  *      zoom: true
32  *   });
33  *
34  * The default is to synchronize both of these.
35  *
36  * Instead of passing one Dygraph object as each parameter, you may also pass an
37  * array of dygraphs:
38  *
39  *   var sync = Dygraph.synchronize([g1, g2, g3], {
40  *      selection: false,
41  *      zoom: true
42  *   });
43  *
44  * You may also set `range: false` if you wish to only sync the x-axis.
45  * The `range` option has no effect unless `zoom` is true (the default).
46  *
47  * Original source: https://github.com/danvk/dygraphs/blob/master/src/extras/synchronizer.js
48  * at commit b55a71d768d2f8de62877c32b3aec9e9975ac389
49  *
50  * Copyright (c) 2009 Dan Vanderkam
51  *
52  * Permission is hereby granted, free of charge, to any person
53  * obtaining a copy of this software and associated documentation
54  * files (the "Software"), to deal in the Software without
55  * restriction, including without limitation the rights to use,
56  * copy, modify, merge, publish, distribute, sublicense, and/or sell
57  * copies of the Software, and to permit persons to whom the
58  * Software is furnished to do so, subject to the following
59  * conditions:
60  *
61  * The above copyright notice and this permission notice shall be
62  * included in all copies or substantial portions of the Software.
63  *
64  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
65  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
66  * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
67  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
68  * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
69  * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
70  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
71  * OTHER DEALINGS IN THE SOFTWARE.
72  */
73 (function() {
74 /* global Dygraph:false */
75 'use strict';
76
77 var Dygraph;
78 if (window.Dygraph) {
79   Dygraph = window.Dygraph;
80 } else if (typeof(module) !== 'undefined') {
81   Dygraph = require('../dygraph');
82 }
83
84 var synchronize = function(/* dygraphs..., opts */) {
85   if (arguments.length === 0) {
86     throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
87   }
88
89   var OPTIONS = ['selection', 'zoom', 'range'];
90   var opts = {
91     selection: true,
92     zoom: true,
93     range: true
94   };
95   var dygraphs = [];
96   var prevCallbacks = [];
97
98   var parseOpts = function(obj) {
99     if (!(obj instanceof Object)) {
100       throw 'Last argument must be either Dygraph or Object.';
101     } else {
102       for (var i = 0; i < OPTIONS.length; i++) {
103         var optName = OPTIONS[i];
104         if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
105       }
106     }
107   };
108
109   if (arguments[0] instanceof Dygraph) {
110     // Arguments are Dygraph objects.
111     for (var i = 0; i < arguments.length; i++) {
112       if (arguments[i] instanceof Dygraph) {
113         dygraphs.push(arguments[i]);
114       } else {
115         break;
116       }
117     }
118     if (i < arguments.length - 1) {
119       throw 'Invalid invocation of Dygraph.synchronize(). ' +
120             'All but the last argument must be Dygraph objects.';
121     } else if (i == arguments.length - 1) {
122       parseOpts(arguments[arguments.length - 1]);
123     }
124   } else if (arguments[0].length) {
125     // Invoked w/ list of dygraphs, options
126     for (var i = 0; i < arguments[0].length; i++) {
127       dygraphs.push(arguments[0][i]);
128     }
129     if (arguments.length == 2) {
130       parseOpts(arguments[1]);
131     } else if (arguments.length > 2) {
132       throw 'Invalid invocation of Dygraph.synchronize(). ' +
133             'Expected two arguments: array and optional options argument.';
134     }  // otherwise arguments.length == 1, which is fine.
135   } else {
136     throw 'Invalid invocation of Dygraph.synchronize(). ' +
137           'First parameter must be either Dygraph or list of Dygraphs.';
138   }
139
140   if (dygraphs.length < 2) {
141     throw 'Invalid invocation of Dygraph.synchronize(). ' +
142           'Need two or more dygraphs to synchronize.';
143   }
144
145   var readycount = dygraphs.length;
146   for (var i = 0; i < dygraphs.length; i++) {
147     var g = dygraphs[i];
148     g.ready( function() {
149       if (--readycount == 0) {
150         // store original callbacks
151         var callBackTypes = ['drawCallback', 'highlightCallback', 'unhighlightCallback'];
152         for (var j = 0; j < dygraphs.length; j++) {
153           if (!prevCallbacks[j]) {
154             prevCallbacks[j] = {};
155           }
156           for (var k = callBackTypes.length - 1; k >= 0; k--) {
157             prevCallbacks[j][callBackTypes[k]] = dygraphs[j].getFunctionOption(callBackTypes[k]);
158           }
159         }
160
161         // Listen for draw, highlight, unhighlight callbacks.
162         if (opts.zoom) {
163           attachZoomHandlers(dygraphs, opts, prevCallbacks);
164         }
165
166         if (opts.selection) {
167           attachSelectionHandlers(dygraphs, prevCallbacks);
168         }
169       }
170     });
171   }
172
173   return {
174     detach: function() {
175       for (var i = 0; i < dygraphs.length; i++) {
176         var g = dygraphs[i];
177         if (opts.zoom) {
178           g.updateOptions({drawCallback: prevCallbacks[i].drawCallback});
179         }
180         if (opts.selection) {
181           g.updateOptions({
182             highlightCallback: prevCallbacks[i].highlightCallback,
183             unhighlightCallback: prevCallbacks[i].unhighlightCallback
184           });
185         }
186       }
187       // release references & make subsequent calls throw.
188       dygraphs = null;
189       opts = null;
190       prevCallbacks = null;
191     }
192   };
193 };
194
195 function arraysAreEqual(a, b) {
196   if (!Array.isArray(a) || !Array.isArray(b)) return false;
197   var i = a.length;
198   if (i !== b.length) return false;
199   while (i--) {
200     if (a[i] !== b[i]) return false;
201   }
202   return true;
203 }
204
205 function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
206   var block = false;
207   for (var i = 0; i < gs.length; i++) {
208     var g = gs[i];
209     g.updateOptions({
210       drawCallback: function(me, initial) {
211         if (block || initial) return;
212         block = true;
213         var opts = {
214           dateWindow: me.xAxisRange()
215         };
216         if (syncOpts.range) opts.valueRange = me.yAxisRange();
217
218         for (var j = 0; j < gs.length; j++) {
219           if (gs[j] == me) {
220             if (prevCallbacks[j] && prevCallbacks[j].drawCallback) {
221               prevCallbacks[j].drawCallback.apply(this, arguments);
222             }
223             continue;
224           }
225
226           // Only redraw if there are new options
227           if (arraysAreEqual(opts.dateWindow, gs[j].getOption('dateWindow')) && 
228               arraysAreEqual(opts.valueRange, gs[j].getOption('valueRange'))) {
229             continue;
230           }
231
232           gs[j].updateOptions(opts);
233         }
234         block = false;
235       }
236     }, true /* no need to redraw */);
237   }
238 }
239
240 function attachSelectionHandlers(gs, prevCallbacks) {
241   var block = false;
242   for (var i = 0; i < gs.length; i++) {
243     var g = gs[i];
244
245     g.updateOptions({
246       highlightCallback: function(event, x, points, row, seriesName) {
247         if (block) return;
248         block = true;
249         var me = this;
250         for (var i = 0; i < gs.length; i++) {
251           if (me == gs[i]) {
252             if (prevCallbacks[i] && prevCallbacks[i].highlightCallback) {
253               prevCallbacks[i].highlightCallback.apply(this, arguments);
254             }
255             continue;
256           }
257           var idx = gs[i].getRowForX(x);
258           if (idx !== null) {
259             gs[i].setSelection(idx, seriesName);
260           }
261         }
262         block = false;
263       },
264       unhighlightCallback: function(event) {
265         if (block) return;
266         block = true;
267         var me = this;
268         for (var i = 0; i < gs.length; i++) {
269           if (me == gs[i]) {
270             if (prevCallbacks[i] && prevCallbacks[i].unhighlightCallback) {
271               prevCallbacks[i].unhighlightCallback.apply(this, arguments);
272             }
273             continue;
274           }
275           gs[i].clearSelection();
276         }
277         block = false;
278       }
279     }, true /* no need to redraw */);
280   }
281 }
282
283 Dygraph.synchronize = synchronize;
284
285 })();
286
287 // Copyright (C) The Arvados Authors. All rights reserved.
288 //
289 // SPDX-License-Identifier: AGPL-3.0
290
291 window.onload = function() {
292     var charts = {};
293     var fmt = {
294         iso: function(y) {
295             var s='';
296             if (y > 1000000000000000) { y=y/1000000000000000; s='P'; }
297             else if (y > 1000000000000) { y=y/1000000000000; s='T'; }
298             else if (y > 1000000000) { y=y/1000000000; s='G'; }
299             else if (y > 1000000) { y=y/1000000; s='M'; }
300             else if (y > 1000) { y=y/1000; s='K'; }
301             return y.toFixed(2).replace(/\.0+$/, '')+s;
302         },
303         time: function(s) {
304             var ret = ''
305             if (s >= 86400) ret += Math.floor(s/86400) + 'd'
306             if (s >= 3600) ret += Math.floor(s/3600)%24 + 'h'
307             if (s >= 60) ret += Math.floor(s/60)%60 + 'm'
308             ret += Math.floor(s)%60 + 's'
309             // finally, strip trailing zeroes: 1d0m0s -> 1d
310             return ret.replace(/(\D)(0\D)*$/, '$1')
311         },
312         date: function(s, opts, sth, dg, idk, excludeHour) {
313             var date = new Date(s);
314             var options = {month: 'numeric', day: 'numeric'};
315             if (!excludeHour) {
316                 options.hour = 'numeric';
317                 options.minute = 'numeric';
318                 options.hour12 = false;
319             }
320             var r = new Intl.DateTimeFormat(undefined, options).format(date);
321             return r;
322         },
323     }
324     var ticker = {
325         time: function(min, max, pixels, opts, dg) {
326             var max_ticks = Math.floor(pixels / (opts('axisLabelWidth')+opts('pixelsPerLabel')/2))
327             var natural = [1, 5, 10, 30, 60,
328                            120, 300, 600, 1800, 3600,
329                            7200, 14400, 43200, 86400]
330             var interval = natural.shift()*1000
331             while (max>min && (max-min)/interval > max_ticks) {
332                 interval = (natural.shift()*1000) || (interval * 2)
333             }
334             var ticks = []
335             var excludeHour = false;
336             var date = new Date(min);
337             // need to take the seconds since midnight and then round off to the nearest interval.
338             var millisecondsSinceMidnight = (date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds()) * 1000;
339             if (interval >= 86400000) {
340                 excludeHour = true;
341             } else {
342                 var roundedOff = Math.ceil(millisecondsSinceMidnight/interval)*interval;
343                 min = (min - millisecondsSinceMidnight) + roundedOff;
344             }
345             //for (var i=Math.ceil(min/interval)*interval; i<=max; i+=interval) {
346             for (var i=min; i<=max; i+=interval) {
347                 ticks.push({v: i, label: opts('axisLabelFormatter')(i, opts, "", false, false, excludeHour)})
348             }
349             return ticks
350         },
351     }
352     chartdata.forEach(function(section, section_idx) {
353         var chartDiv = document.getElementById("chart");
354         section.charts.forEach(function(chart, chart_idx) {
355             // Skip chart if every series has zero data points
356             if (0 == chart.data.reduce(function(len, series) {
357                 return len + series.length;
358             }, 0)) {
359                 return;
360             }
361             var id = 'chart-'+section_idx+'-'+chart_idx;
362             var div = document.createElement('div');
363             div.setAttribute('id', id);
364             div.setAttribute('style', 'width: 100%; height: 250px');
365             chartDiv.appendChild(div);
366             chart.options.valueFormatter = function(y) {
367             }
368             chart.options.axes = {
369                 x: {
370                     axisLabelFormatter: fmt.date,
371                     valueFormatter: fmt.date,
372                     ticker: ticker.time,
373                     axisLabelWidth: 60,
374                     pixelsPerLabel: 20,
375                 },
376                 y: {
377                     axisLabelFormatter: fmt.iso,
378                     valueFormatter: fmt.iso,
379                 },
380             }
381             var div2 = document.createElement('div');
382             div2.setAttribute('style', 'width: 150px; height: 250px');
383             chart.options.labelsDiv = div2;
384             chart.options.labelsSeparateLines = true;
385
386             var div3 = document.createElement('div');
387             div3.setAttribute('style', 'display: flex; padding-bottom: 16px');
388             div3.appendChild(div);
389             div3.appendChild(div2);
390             chartDiv.appendChild(div3);
391
392             charts[id] = new Dygraph(div, chart.data, chart.options);
393         });
394     });
395
396     var sync = Dygraph.synchronize(Object.values(charts), {range: false});
397
398     if (typeof window.debug === 'undefined')
399         window.debug = {};
400     window.debug.charts = charts;
401 };
402
403 /**
404  * Copyright Jonas Earendel. All rights reserved.
405  * SPDX-License-Identifier: Unlicense
406  *
407  * sortable v3.2.3
408  *
409  * https://www.npmjs.com/package/sortable-tablesort
410  * https://github.com/tofsjonas/sortable
411  *
412  * Makes html tables sortable, No longer ie9+ 😢
413  *
414  * Styling is done in css.
415  *
416  * Copyleft 2017 Jonas Earendel
417  *
418  * This is free and unencumbered software released into the public domain.
419  *
420  * Anyone is free to copy, modify, publish, use, compile, sell, or
421  * distribute this software, either in source code form or as a compiled
422  * binary, for any purpose, commercial or non-commercial, and by any
423  * means.
424  *
425  * In jurisdictions that recognize copyright laws, the author or authors
426  * of this software dedicate any and all copyright interest in the
427  * software to the public domain. We make this dedication for the benefit
428  * of the public at large and to the detriment of our heirs and
429  * successors. We intend this dedication to be an overt act of
430  * relinquishment in perpetuity of all present and future rights to this
431  * software under copyright law.
432  *
433  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
434  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
435  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
436  * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
437  * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
438  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
439  * OTHER DEALINGS IN THE SOFTWARE.
440  *
441  * For more information, please refer to <http://unlicense.org>
442  *
443  */
444 document.addEventListener('click', function (e) {
445     try {
446         // allows for elements inside TH
447         function findElementRecursive(element, tag) {
448             return element.nodeName === tag ? element : findElementRecursive(element.parentNode, tag);
449         }
450         var ascending_table_sort_class = 'asc';
451         var no_sort_class = 'no-sort';
452         var null_last_class = 'n-last';
453         var table_class_name = 'sortable';
454         var alt_sort_1 = e.shiftKey || e.altKey;
455         var element = findElementRecursive(e.target, 'TH');
456         var tr = element.parentNode;
457         var thead = tr.parentNode;
458         var table = thead.parentNode;
459         function getValue(element) {
460             var _a;
461             var value = alt_sort_1 ? element.dataset.sortAlt : (_a = element.dataset.sort) !== null && _a !== void 0 ? _a : element.textContent;
462             return value;
463         }
464         if (thead.nodeName === 'THEAD' && // sortable only triggered in `thead`
465             table.classList.contains(table_class_name) &&
466             !element.classList.contains(no_sort_class) // .no-sort is now core functionality, no longer handled in CSS
467         ) {
468             var column_index_1;
469             var nodes = tr.cells;
470             var tiebreaker_1 = +element.dataset.sortTbr;
471             // Reset thead cells and get column index
472             for (var i = 0; i < nodes.length; i++) {
473                 if (nodes[i] === element) {
474                     column_index_1 = +element.dataset.sortCol || i;
475                 }
476                 else {
477                     nodes[i].setAttribute('aria-sort', 'none');
478                 }
479             }
480             var direction = 'descending';
481             if (element.getAttribute('aria-sort') === 'descending' ||
482                 (table.classList.contains(ascending_table_sort_class) && element.getAttribute('aria-sort') !== 'ascending')) {
483                 direction = 'ascending';
484             }
485             // Update the `th` class accordingly
486             element.setAttribute('aria-sort', direction);
487             var reverse_1 = direction === 'ascending';
488             var sort_null_last_1 = table.classList.contains(null_last_class);
489             var compare_1 = function (a, b, index) {
490                 var x = getValue(b.cells[index]);
491                 var y = getValue(a.cells[index]);
492                 if (sort_null_last_1) {
493                     if (x === '' && y !== '') {
494                         return -1;
495                     }
496                     if (y === '' && x !== '') {
497                         return 1;
498                     }
499                 }
500                 // Before comparing, clean up formatted numbers that may have a leading dollar sign and/or commas.
501                 x = x.replace("$", "").replace(",", "");
502                 y = y.replace("$", "").replace(",", "");
503                 var temp = +x - +y;
504                 var bool = isNaN(temp) ? x.localeCompare(y) : temp;
505                 return reverse_1 ? -bool : bool;
506             };
507             // loop through all tbodies and sort them
508             for (var i = 0; i < table.tBodies.length; i++) {
509                 var org_tbody = table.tBodies[i];
510                 // Put the array rows in an array, so we can sort them...
511                 var rows = [].slice.call(org_tbody.rows, 0);
512                 // Sort them using Array.prototype.sort()
513                 rows.sort(function (a, b) {
514                     var bool = compare_1(a, b, column_index_1);
515                     return bool === 0 && !isNaN(tiebreaker_1) ? compare_1(a, b, tiebreaker_1) : bool;
516                 });
517                 // Make an empty clone
518                 var clone_tbody = org_tbody.cloneNode();
519                 // Put the sorted rows inside the clone
520                 clone_tbody.append.apply(clone_tbody, rows);
521                 // And finally replace the unsorted tbody with the sorted one
522                 table.replaceChild(clone_tbody, org_tbody);
523             }
524         }
525         // eslint-disable-next-line no-unused-vars
526     }
527     catch (error) {
528         // console.log(error)
529     }
530 });
531
532         </script>
533
534
535     <style>
536         body {
537           background: #fafafa;
538           font-family: "Roboto", "Helvetica", "Arial", sans-serif;
539           font-size: 0.875rem;
540           color: rgba(0, 0, 0, 0.87);
541           font-weight: 400;
542         }
543         .card {
544           background: #ffffff;
545           box-shadow: 0px 1px 5px 0px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 3px 1px -2px rgba(0,0,0,0.12);
546           border-radius: 4px;
547           margin: 20px;
548         }
549         .content {
550           padding: 2px 16px 8px 16px;
551         }
552         table {
553           border-spacing: 0px;
554         }
555         tr {
556           height: 36px;
557           text-align: left;
558         }
559         th {
560           padding-right: 4em;
561           border-top: 1px solid rgba(224, 224, 224, 1);
562         }
563         td {
564           padding-right: 2em;
565           border-top: 1px solid rgba(224, 224, 224, 1);
566         }
567         #chart {
568           margin-left: -20px;
569         }
570     </style>
571     
572
573 <style>
574 @charset "UTF-8";
575 .sortable thead th:not(.no-sort) {
576   cursor: pointer;
577 }
578 .sortable thead th:not(.no-sort)::after, .sortable thead th:not(.no-sort)::before {
579   transition: color 0.1s ease-in-out;
580   font-size: 1.2em;
581   color: transparent;
582 }
583 .sortable thead th:not(.no-sort)::after {
584   margin-left: 3px;
585   content: "▸";
586 }
587 .sortable thead th:not(.no-sort):hover::after {
588   color: inherit;
589 }
590 .sortable thead th:not(.no-sort)[aria-sort=descending]::after {
591   color: inherit;
592   content: "▾";
593 }
594 .sortable thead th:not(.no-sort)[aria-sort=ascending]::after {
595   color: inherit;
596   content: "▴";
597 }
598 .sortable thead th:not(.no-sort).indicator-left::after {
599   content: "";
600 }
601 .sortable thead th:not(.no-sort).indicator-left::before {
602   margin-right: 3px;
603   content: "▸";
604 }
605 .sortable thead th:not(.no-sort).indicator-left:hover::before {
606   color: inherit;
607 }
608 .sortable thead th:not(.no-sort).indicator-left[aria-sort=descending]::before {
609   color: inherit;
610   content: "▾";
611 }
612 .sortable thead th:not(.no-sort).indicator-left[aria-sort=ascending]::before {
613   color: inherit;
614   content: "▴";
615 }
616
617 table.aggtable td:nth-child(2) {
618   text-align: right;
619 }
620
621 table.active-projects td:nth-child(4),
622 table.active-projects td:nth-child(5) {
623   text-align: right;
624   padding-right: 6em;
625 }
626
627 table.single-project td:nth-child(3),
628 table.single-project td:nth-child(4) {
629   text-align: right;
630   padding-right: 6em;
631 }
632
633 table.active-projects th:nth-child(4),
634 table.active-projects th:nth-child(5) {
635   text-align: left;
636 }
637
638 table.project td:nth-child(3),
639 table.project td:nth-child(4),
640 table.project td:nth-child(5),
641 table.project td:nth-child(6),
642 table.project td:nth-child(7) {
643   text-align: right;
644   padding-right: 6em;
645 }
646
647 table.project th:nth-child(3),
648 table.project th:nth-child(4),
649 table.project th:nth-child(5),
650 table.project th:nth-child(6),
651 table.project th:nth-child(7) {
652   text-align: left;
653 }
654 </style>
655
656 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.0.0/dygraph.min.css">
657
658
659
660
661   </head>
662
663   <body>
664   <div class="card">
665     <div class="content">
666       <h1>Cluster report for xzzz1 from 2024-04-04 to 2024-04-06</h1>
667     </div>
668   </div>
669
670
671                 <div class="card">
672                   <div class="content">
673 <h2>Cluster status as of 2024-04-06</h2>
674             <table class='aggtable'><tbody>
675             <tr><th><a href="https://xzzz1.arvadosapi.com/users">Total users</a></th><td>4</td></tr>
676             <tr><th>Total projects</th><td>6</td></tr>
677             
678             <tr><th>Total data under management</th> <td>0.003 KB</td></tr>
679             <tr><th>Total storage usage</th> <td>0.003 KB</td></tr>
680             <tr><th>Deduplication ratio</th> <td>1.0</td></tr>
681             <tr><th>Approximate monthly storage cost</th> <td>$0.00</td></tr>
682                 
683             </tbody></table>
684             <p>See <a href="#prices">note on usage and cost calculations</a> for details on how costs are calculated.</p>
685             
686                   </div>
687                 </div>
688
689                 <div class="card">
690                   <div class="content">
691 <h2>Activity and cost over the 2 day period 2024-04-04 to 2024-04-06</h2>
692         <table class='aggtable'><tbody>
693         <tr><th>Active users</th> <td>1</td></tr>
694         <tr><th><a href="#Active_Projects">Active projects</a></th> <td>1</td></tr>
695         <tr><th>Workflow runs</th> <td>1</td></tr>
696         <tr><th>Compute used</th> <td>1.5 hours</td></tr>
697         <tr><th>Compute cost</th> <td>$0.23</td></tr>
698         <tr><th>Storage cost</th> <td>$0.00</td></tr>
699         </tbody></table>
700         <p>See <a href="#prices">note on usage and cost calculations</a> for details on how costs are calculated.</p>
701         
702                   </div>
703                 </div>
704
705                 <div class="card">
706                   <div class="content">
707
708                 <div id="chart"></div>
709             
710                   </div>
711                 </div>
712
713                 <div class="card">
714                   <div class="content">
715
716             <a id="Active_Projects"><h2>Active Projects</h2></a>
717             <table class='sortable active-projects'>
718             <thead><tr><th>Project</th> <th>Users</th> <th>Active</th> <th>Compute usage (hours)</th> <th>Compute cost</th> </tr></thead>
719             <tbody><tr><td><a href="#WGS chr19 test for 2.7.2~rc3">WGS chr19 test for 2.7.2~rc3</a></td><td>User1</td> <td>2024-04-05 to 2024-08-22</td> <td>1.5</td> <td>$0.23</td></tr></tbody>
720             </table>
721             <p>See <a href="#prices">note on usage and cost calculations</a> for details on how costs are calculated.</p>
722             
723                   </div>
724                 </div>
725
726                 <div class="card">
727                   <div class="content">
728 <a id="WGS chr19 test for 2.7.2~rc3"></a><a href="https://xzzz1.arvadosapi.com/projects/pirca-j7d0g-cukk4aw4iamj90c"><h2>WGS chr19 test for 2.7.2~rc3</h2></a>
729
730                 <table class='sortable single-project'>
731                 <thead><tr> <th>Users</th> <th>Active</th> <th>Compute usage (hours)</th> <th>Compute cost</th> </tr></thead>
732                 <tbody><tr><td>User1</td> <td>2024-04-05 to 2024-08-22</td> <td>1.5</td> <td>$0.23</td></tr></tbody>
733                 </table>
734
735                 <table class='sortable project'>
736                 <thead><tr><th>Workflow run count</th> <th>Workflow name</th> <th>Median runtime</th> <th>Mean runtime</th> <th>Median cost per run</th> <th>Mean cost per run</th> <th>Sum cost over runs</th></tr></thead>
737                 <tbody>
738                 
739                 <tr><td>1</td> <td>WGS processing workflow scattered over samples (v1.1-2-gcf002b3)</td> <td>1:19:21</td> <td>1:19:21</td> <td>$1.37</td> <td>$1.37</td> <td>$1.37</td></tr>
740                 
741                 </tbody></table>
742                 
743                   </div>
744                 </div>
745
746                 <div class="card">
747                   <div class="content">
748
749         <h2 id="prices">Note on usage and cost calculations</h2>
750
751         <div style="max-width: 60em">
752
753         <p>The numbers presented in this report are estimates and will
754         not perfectly match your cloud bill.  Nevertheless this report
755         should be useful for identifying your main cost drivers.</p>
756
757         <h3>Storage</h3>
758
759         <p>"Total data under management" is what you get if you add up
760         all blocks referenced by all collections in Workbench, without
761         considering deduplication.</p>
762
763         <p>"Total storage usage" is the actual underlying storage
764         usage, accounting for data deduplication.</p>
765
766         <p>Storage costs are based on AWS "S3 Standard"
767         described on the <a href="https://aws.amazon.com/s3/pricing/">Amazon S3 pricing</a> page:</p>
768
769         <ul>
770         <li>$0.023 per GB / Month for the first 50 TB</li>
771         <li>$0.022 per GB / Month for the next 450 TB</li>
772         <li>$0.021 per GB / Month over 500 TB</li>
773         </ul>
774
775         <p>Finally, this only the base storage cost, and does not
776         include any fees associated with S3 API usage.  However, there
777         are generally no ingress/egress fees if your Arvados instance
778         and S3 bucket are in the same region, which is the normal
779         recommended configuration.</p>
780
781         <h3>Compute</h3>
782
783         <p>"Compute usage" are instance-hours used in running
784         workflows.  Because multiple steps may run in parallel on
785         multiple instances, a workflow that completes in four hours
786         but runs parallel steps on five instances, would be reported
787         as using 20 instance hours.</p>
788
789         <p>"Runtime" is the actual wall clock time that it took to
790         complete a workflow.  This does not include time spent in the
791         queue for the workflow itself, but does include queuing time
792         of individual workflow steps.</p>
793
794         <p>Computational costs are derived from Arvados cost
795         calculations of container runs.  For on-demand instances, this
796         uses the prices from the InstanceTypes section of the Arvado
797         config file, set by the system administrator.  For spot
798         instances, this uses current spot prices retrieved on the fly
799         the AWS API.</p>
800
801         <p>Be aware that the cost calculations are only for the time
802         the container is running and only do not take into account the
803         overhead of launching instances or idle time between scheduled
804         tasks or prior to automatic shutdown.</p>
805
806         </div>
807         
808                   </div>
809                 </div>
810
811   </body>
812 </html>
813