Skip to content

Commit

Permalink
Compute stats for whole queues (#198)
Browse files Browse the repository at this point in the history
* Extract stats calculations from continuousprint_jobs.js, make also usable on whole queues

* Add HTML for viewing queue rollups, make selectively visible

* Fix tests, fix batch selection, auto-hide mass estimates when zero
  • Loading branch information
smartin015 authored Jan 18, 2023
1 parent 696d543 commit 970f699
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 148 deletions.
1 change: 1 addition & 0 deletions continuousprint/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def __init__(self, setting, default):
"js/continuousprint_api.js",
"js/continuousprint_history_row.js",
"js/continuousprint_set.js",
"js/continuousprint_stats.js",
"js/continuousprint_job.js",
"js/continuousprint_queue.js",
"js/continuousprint_viewmodel.js",
Expand Down
140 changes: 9 additions & 131 deletions continuousprint/static/js/continuousprint_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ if (typeof ko === "undefined" || ko === null) {
}
if (typeof CPSet === "undefined" || CPSet === null) {
CPSet = require('./continuousprint_set');
CPStats = require('./continuousprint_stats');
CP_STATS_DIMENSIONS={
completed: null,
count: null,
remaining: null,
total: null,
};
}

// jobs and sets are derived from self.queue, but they must be
// observableArrays in order for Sortable to be able to reorder it.
function CPJob(obj, peers, api, profile, materials) {
function CPJob(obj, peers, api, profile, materials, stats_dimensions=CP_STATS_DIMENSIONS) {
if (api === undefined) {
throw Error("API must be provided when creating CPJob");
}
Expand Down Expand Up @@ -113,42 +120,6 @@ function CPJob(obj, peers, api, profile, materials) {
api.edit(api.JOB, data, self._update);
}

self._safeParse = function(v) {
v = parseInt(v, 10);
if (isNaN(v)) {
return 0;
}
return v;
}


self.humanize = function(num, unit="") {
// Humanizes numbers by condensing and adding units
let v = '';
if (num < 1000) {
v = (num % 1 === 0) ? num : num.toFixed(1);
} else if (num < 100000) {
let k = (num/1000);
v = ((k % 1 === 0) ? k : k.toFixed(1)) + 'k';
}
return v + unit;
};

self.humanTime = function(s) {
// Humanizes time values; parameter is seconds
if (s < 60) {
return Math.round(s) + 's';
} else if (s < 3600) {
return Math.round(s/60) + 'm';
} else if (s < 86400) {
let h = s/3600;
return ((h % 1 === 0) ? h : h.toFixed(1)) + 'h';
} else {
let d = s/86400;
return ((d % 1 === 0) ? d : d.toFixed(1)) + 'd';
}
};

self.getMaterialLinearMasses = ko.computed(function() {
let result = [];
for (let m of materials()) {
Expand All @@ -162,101 +133,8 @@ function CPJob(obj, peers, api, profile, materials) {
return result;
});

self.raw_stats = ko.computed(function() {
let result = {completed: 0, remaining: 0, count: 0};
for (let qs of self.sets()) {
if (!qs.profile_matches()) {
continue;
}
result.remaining += self._safeParse(qs.remaining());
result.count += self._safeParse(qs.count());
result.completed += self._safeParse(qs.completed());
}
return result;
});

self.totals = ko.computed(function() {
let r = [
{legend: 'Total items', title: null},
{legend: 'Total time', title: "Uses Octoprint's file analysis estimate; may be inaccurate"},
{legend: 'Total mass', title: "Mass is calculated using active spool(s) in SpoolManager"},
];

let linmasses = self.getMaterialLinearMasses();

for (let t of r) {
t.count = 0;
t.completed = 0;
t.remaining = 0;
t.total = 0;
t.error = 0;
}

for (let qs of self.sets()) {
if (!qs.profile_matches()) {
continue;
}

let rem = self._safeParse(qs.remaining())
let tot = self._safeParse(qs.length_remaining());
let count = self._safeParse(qs.count());
let cplt = self._safeParse(qs.completed());

let meta = qs.metadata;
let ept = meta && meta.estimatedPrintTime
let len = meta && meta.filamentLengths;

// Update print count totals
r[0].remaining += rem;
r[0].total += tot;
r[0].count += count;
r[0].completed += cplt;

if (ept === null || ept === undefined) {
r[1].error += 1;
} else {
r[1].remaining += rem * ept;
r[1].total += tot * ept
r[1].count += count * ept;
r[1].completed += cplt * ept;
}

if (len === null || len === undefined || len.length === 0) {
r[2].error += 1;
} else {
let mass = 0;
for (let i = 0; i < len.length; i++) {
mass += linmasses[i] * len[i];
}

if (!isNaN(mass)) {
r[2].remaining += rem * mass;
r[2].total += tot * mass;
r[2].count += count * mass;
r[2].completed += cplt * mass;
} else {
r[2].error += 1;
}
}

}
// Assign error texts
r[0].error = '';
r[1].error = (r[1].error > 0) ? `${r[1].error} sets missing time estimates` : '';
r[2].error = (r[2].error > 0) ? `${r[1].error} errors calculating mass` : '';

for (let k of ['remaining', 'total', 'count', 'completed']) {
r[0][k] = self.humanize(r[0][k]);
r[1][k] = self.humanTime(r[1][k]);
r[2][k] = self.humanize(r[2][k], 'g');
}

// Hide mass details if linmasses is empty (implies SpoolManager not set up)
if (linmasses.length === 0) {
r.splice(2,1);
}

return r;
return new CPStats(() => [self], stats_dimensions);
});

self.checkFraction = ko.computed(function() {
Expand Down
6 changes: 4 additions & 2 deletions continuousprint/static/js/continuousprint_job.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ test('onSetModified existing', () => {
});

test('totals', () => {
let j = new Job({count: 3, completed: 2, remaining: 1, sets: sets()}, [], api(), prof(), mats());
let j = new Job({
count: 3, completed: 2, remaining: 1, sets: sets()
}, [], api(), prof(), mats());

let t = j.totals();
let t = j.totals().values_humanized();
expect(t[0]).toStrictEqual({
completed: "2", // sets have 1/2 completed this run
count: "4", // 2 sets each with count=2
Expand Down
25 changes: 19 additions & 6 deletions continuousprint/static/js/continuousprint_queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ if (typeof CPJob === "undefined" || CPJob === null) {
// In the testing environment, dependencies must be manually imported
ko = require('knockout');
CPJob = require('./continuousprint_job');
CPStats = require('./continuousprint_stats');
CP_STATS_DIMENSIONS={
completed: null,
count: null,
remaining: null,
total: null,
};
log = {
"getLogger": () => {return console;}
};
}

function CPQueue(data, api, files, profile, materials) {
function CPQueue(data, api, files, profile, materials, stats_dimensions=CP_STATS_DIMENSIONS) {
var self = this;
self.api = api;
self.files = files;
Expand All @@ -23,14 +30,15 @@ function CPQueue(data, api, files, profile, materials) {
self.addr = data.addr;
self.jobs = ko.observableArray([]);
self._pushJob = function(jdata) {
self.jobs.push(new CPJob(jdata, data.peers, self.api, profile, materials));
self.jobs.push(new CPJob(jdata, data.peers, self.api, profile, materials, stats_dimensions));
};
for (let j of data.jobs) {
self._pushJob(j);
}
self.shiftsel = ko.observable(-1);
self.details = ko.observable("");
self.fullDetails = ko.observable("");
self.showStats = ko.observable(true);
self.ready = ko.observable(data.name === 'local' || Object.keys(data.peers).length > 0);
if (self.addr !== null && data.peers !== undefined) {
let pkeys = Object.keys(data.peers);
Expand Down Expand Up @@ -95,18 +103,19 @@ function CPQueue(data, api, files, profile, materials) {
break;
case "Unstarted Jobs":
for (let j of self.jobs()) {
j.onChecked(j.sets().length !== 0 && j.raw_stats().completed === 0);
let t = j.totals().values()[0];
j.onChecked(j.sets().length !== 0 && t.completed === 0 && j.completed() === 0);
}
break;
case "Incomplete Jobs":
for (let j of self.jobs()) {
let t = j.raw_stats();
j.onChecked(t.remaining > 0 && t.remaining < t.count);
let t = j.totals().values()[0];
j.onChecked(j.remaining() > 0 && (j.completed() > 0 || t.completed > 0));
}
break;
case "Completed Jobs":
for (let j of self.jobs()) {
j.onChecked(j.sets().length !== 0 && j.raw_stats().remaining == 0);
j.onChecked(j.sets().length !== 0 && j.remaining() === 0);
}
break;
default:
Expand Down Expand Up @@ -158,6 +167,10 @@ function CPQueue(data, api, files, profile, materials) {
e.preventDefault();
}

self.totals = ko.computed(function() {
return new CPStats(self.jobs, stats_dimensions);
});

// *** ko template methods ***
self._getSelections = function() {
let jobs = [];
Expand Down
2 changes: 1 addition & 1 deletion continuousprint/static/js/continuousprint_queue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ test('setCount', () => {
describe('batchSelect', () => {
let v = init(njobs=4);
v.jobs()[0].sets([]); // job 1 is empty
// job 2 is unstarted; no action needed
// job 2 (idx 1) is unstarted; no action needed
v.jobs()[2].sets()[0].count(3); // Job 3 is incomplete, set 5 is incomplete
v.jobs()[2].sets()[0].completed(1);
v.jobs()[2].sets()[0].remaining(2);
Expand Down
Loading

0 comments on commit 970f699

Please sign in to comment.