use a sliding window over counter intervals

This commit is contained in:
Jake McDermott
2018-06-05 09:56:20 -04:00
parent 1c414789fb
commit cbae7efdd5
16 changed files with 800 additions and 1038 deletions

View File

@@ -1,144 +1,110 @@
const PAGE_LIMIT = 5; const API_PAGE_SIZE = 200;
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const ORDER_BY = 'counter';
const BASE_PARAMS = { const BASE_PARAMS = {
order_by: 'start_line',
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
order_by: ORDER_BY,
}; };
const merge = (...objs) => _.merge({}, ...objs); const merge = (...objs) => _.merge({}, ...objs);
const getInitialState = params => ({
results: [],
count: 0,
previous: 1,
page: 1,
next: 1,
last: 1,
params: merge(BASE_PARAMS, params),
});
function JobEventsApiService ($http, $q) { function JobEventsApiService ($http, $q) {
this.init = (endpoint, params) => { this.init = (endpoint, params) => {
this.keys = [];
this.cache = {};
this.pageSizes = {};
this.endpoint = endpoint; this.endpoint = endpoint;
this.state = getInitialState(params); this.params = merge(BASE_PARAMS, params);
this.state = { current: 0, count: 0 };
}; };
this.getLastPage = count => Math.ceil(count / this.state.params.page_size); this.fetch = () => this.getFirst().then(() => this);
this.clearCache = () => {
delete this.cache;
delete this.keys;
delete this.pageSizes;
this.cache = {};
this.keys = [];
this.pageSizes = {};
};
this.fetch = () => this.first().then(() => this);
this.getPage = number => { this.getPage = number => {
if (number < 1 || number > this.state.last) { if (number === 1) return this.getFirst();
return $q.resolve();
}
if (this.cache[number]) { const [low, high] = [1 + PAGE_SIZE * (number - 1), PAGE_SIZE * number];
if (this.pageSizes[number] === PAGE_SIZE) { const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
return this.cache[number];
}
delete this.pageSizes[number]; return $http.get(this.endpoint, { params })
delete this.cache[number];
this.keys.splice(this.keys.indexOf(number));
}
const { params } = this.state;
delete params.page;
params.page = number;
const promise = $http.get(this.endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const { results, count } = data; const { results } = data;
this.state.results = results; this.state.current = number;
this.state.count = count;
this.state.page = number;
this.state.last = this.getLastPage(count);
this.state.previous = Math.max(1, number - 1);
this.state.next = Math.min(this.state.last, number + 1);
this.pageSizes[number] = results.length; return results;
return { results, page: number };
}); });
if (number === 1) {
this.clearCache();
}
this.cache[number] = promise;
this.keys.push(number);
if (this.keys.length > PAGE_LIMIT) {
const remove = this.keys.shift();
delete this.cache[remove];
delete this.pageSizes[remove];
}
return promise;
}; };
this.first = () => this.getPage(1); this.getFirst = () => {
this.next = () => this.getPage(this.state.next); const page = 1;
this.previous = () => this.getPage(this.state.previous); const params = merge(this.params, { page });
this.last = () => { return $http.get(this.endpoint, { params })
const params = merge({}, this.state.params);
delete params.page;
delete params.order_by;
params.page = 1;
params.order_by = '-start_line';
const promise = $http.get(this.endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const { results, count } = data; const { results, count } = data;
const lastPage = this.getLastPage(count);
this.state.count = count;
this.state.current = page;
return results;
});
};
this.getRange = range => {
if (!range) {
return $q.resolve([]);
}
const [low, high] = range;
const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
params.page_size = API_PAGE_SIZE;
return $http.get(this.endpoint, { params })
.then(({ data }) => {
const { results } = data;
const maxCounter = Math.max(results.map(({ counter }) => counter));
this.state.current = Math.ceil(maxCounter / PAGE_SIZE);
return results;
});
};
this.getLast = () => {
const params = merge(this.params, { page: 1, order_by: `-${ORDER_BY}` });
return $http.get(this.endpoint, { params })
.then(({ data }) => {
const { results } = data;
const count = Math.max(...results.map(({ counter }) => counter));
let rotated = results;
if (count > PAGE_SIZE) { if (count > PAGE_SIZE) {
results.splice(count % PAGE_SIZE); rotated = results.splice(count % PAGE_SIZE);
if (results.length > 0) {
rotated = results;
}
} }
results.reverse();
this.state.results = results;
this.state.count = count; this.state.count = count;
this.state.page = lastPage; this.state.current = Math.ceil(count / PAGE_SIZE);
this.state.next = lastPage;
this.state.last = lastPage;
this.state.previous = Math.max(1, this.state.page - 1);
this.clearCache(); return rotated;
return { results, page: lastPage };
}); });
return promise;
}; };
this.getCurrentPageNumber = () => this.state.current;
this.getLastPageNumber = () => Math.ceil(this.state.count / PAGE_SIZE);
this.getPreviousPageNumber = () => Math.max(1, this.state.current - 1);
this.getNextPageNumber = () => Math.min(this.state.current + 1, this.getLastPageNumber());
this.getMaxCounter = () => this.state.count;
this.getNext = () => this.getPage(this.getNextPageNumber());
this.getPrevious = () => this.getPage(this.getPreviousPageNumber());
} }
JobEventsApiService.$inject = [ JobEventsApiService.$inject = ['$http', '$q'];
'$http',
'$q'
];
export default JobEventsApiService; export default JobEventsApiService;

View File

@@ -729,7 +729,7 @@ JobDetailsController.$inject = [
'OutputStrings', 'OutputStrings',
'Wait', 'Wait',
'ParseVariableString', 'ParseVariableString',
'JobStatusService', 'OutputStatusService',
]; ];
export default { export default {

View File

@@ -1,4 +1,5 @@
<!-- todo: styling, css etc. - disposition according to project lib conventions --> <!-- todo: styling, css etc. - disposition according to project lib conventions -->
<!-- todo: further componentization -->
<div class="JobResults-panelHeader"> <div class="JobResults-panelHeader">
<div class="JobResults-panelHeaderText"> {{:: vm.strings.get('details.HEADER')}}</div> <div class="JobResults-panelHeaderText"> {{:: vm.strings.get('details.HEADER')}}</div>
<!-- LEFT PANE HEADER ACTIONS --> <!-- LEFT PANE HEADER ACTIONS -->

View File

@@ -1,235 +0,0 @@
const JOB_END = 'playbook_on_stats';
const MAX_LAG = 120;
function JobEventEngine ($q) {
this.init = ({ resource, scroll, page, onEventFrame, onStart, onStop }) => {
this.resource = resource;
this.scroll = scroll;
this.page = page;
this.lag = 0;
this.count = 0;
this.pageCount = 0;
this.chain = $q.resolve();
this.factors = this.getBatchFactors(this.resource.page.size);
this.state = {
started: false,
paused: false,
pausing: false,
resuming: false,
ending: false,
ended: false,
counting: false,
};
this.hooks = {
onEventFrame,
onStart,
onStop,
};
this.lines = {
used: [],
missing: [],
ready: false,
min: 0,
max: 0
};
};
this.setMinLine = min => {
if (min > this.lines.min) {
this.lines.min = min;
}
};
this.getBatchFactors = size => {
const factors = [1];
for (let i = 2; i <= size / 2; i++) {
if (size % i === 0) {
factors.push(i);
}
}
factors.push(size);
return factors;
};
this.getBatchFactorIndex = () => {
const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
return index > this.factors.length - 1 ? this.factors.length - 1 : index;
};
this.setBatchFrameCount = () => {
const index = this.getBatchFactorIndex();
this.framesPerRender = this.factors[index];
};
this.buffer = data => {
const pageAdded = this.page.addToBuffer(data);
if (pageAdded) {
this.pageCount++;
this.setBatchFrameCount();
if (this.isPausing()) {
this.pause(true);
} else if (this.isResuming()) {
this.resume(true);
}
}
};
this.checkLines = data => {
for (let i = data.start_line; i < data.end_line; i++) {
if (i > this.lines.max) {
this.lines.max = i;
}
this.lines.used.push(i);
}
const missing = [];
for (let i = this.lines.min; i < this.lines.max; i++) {
if (this.lines.used.indexOf(i) === -1) {
missing.push(i);
}
}
if (missing.length === 0) {
this.lines.ready = true;
this.lines.min = this.lines.max + 1;
this.lines.used = [];
} else {
this.lines.ready = false;
}
};
this.pushJobEvent = data => {
this.lag++;
this.chain = this.chain
.then(() => {
if (data.end_line < this.lines.min) {
return $q.resolve();
}
if (!this.isActive()) {
this.start();
} else if (data.event === JOB_END) {
if (this.isPaused()) {
this.end(true);
} else {
this.end();
}
}
this.checkLines(data);
this.buffer(data);
this.count++;
if (!this.isReadyToRender()) {
return $q.resolve();
}
const events = this.page.emptyBuffer();
this.count -= events.length;
return this.renderFrame(events);
})
.then(() => --this.lag);
return this.chain;
};
this.renderFrame = events => this.hooks.onEventFrame(events)
.then(() => {
if (this.scroll.isLocked()) {
this.scroll.scrollToBottom();
}
if (this.isEnding()) {
const lastEvents = this.page.emptyBuffer();
if (lastEvents.length) {
return this.renderFrame(lastEvents);
}
this.end(true);
}
return $q.resolve();
});
this.resume = done => {
if (done) {
this.state.resuming = false;
this.state.paused = false;
} else if (!this.isTransitioning()) {
this.scroll.pause();
this.scroll.lock();
this.scroll.scrollToBottom();
this.state.resuming = true;
this.page.removeBookmark();
}
};
this.pause = done => {
if (done) {
this.state.pausing = false;
this.state.paused = true;
this.scroll.resume();
} else if (!this.isTransitioning()) {
this.scroll.pause();
this.scroll.unlock();
this.state.pausing = true;
this.page.setBookmark();
}
};
this.start = () => {
if (!this.state.ending && !this.state.ended) {
this.state.started = true;
this.scroll.pause();
this.scroll.lock();
this.hooks.onStart();
}
};
this.end = done => {
if (done) {
this.state.ending = false;
this.state.ended = true;
this.scroll.unlock();
this.scroll.resume();
this.hooks.onStop();
return;
}
this.state.ending = true;
};
this.isReadyToRender = () => this.isDone() ||
(!this.isPaused() && this.hasAllLines() && this.isBatchFull());
this.hasAllLines = () => this.lines.ready;
this.isBatchFull = () => this.count % this.framesPerRender === 0;
this.isPaused = () => this.state.paused;
this.isPausing = () => this.state.pausing;
this.isResuming = () => this.state.resuming;
this.isTransitioning = () => this.isActive() && (this.state.pausing || this.state.resuming);
this.isActive = () => this.state.started && !this.state.ended;
this.isEnding = () => this.state.ending;
this.isDone = () => this.state.ended;
}
JobEventEngine.$inject = ['$q'];
export default JobEventEngine;

View File

@@ -1,129 +1,156 @@
/* eslint camelcase: 0 */
let $compile; let $compile;
let $filter;
let $q; let $q;
let $scope; let $scope;
let $state; let $state;
let page;
let render;
let resource; let resource;
let render;
let scroll; let scroll;
let engine;
let status; let status;
let slide;
let stream;
let vm; let vm;
let streaming;
let listeners = [];
function JobsIndexController ( const bufferState = [0, 0]; // [length, count]
_$compile_, const listeners = [];
_$filter_, const rx = [];
_$q_,
_$scope_,
_$state_,
_resource_,
_page_,
_scroll_,
_render_,
_engine_,
_status_,
_strings_,
) {
vm = this || {};
$compile = _$compile_; let following = false;
$filter = _$filter_;
$q = _$q_;
$scope = _$scope_;
$state = _$state_;
resource = _resource_; function bufferInit () {
page = _page_; rx.length = 0;
scroll = _scroll_;
render = _render_;
engine = _engine_;
status = _status_;
vm.strings = _strings_; bufferState[0] = 0;
bufferState[1] = 0;
// Development helper(s)
vm.clear = devClear;
// Expand/collapse
vm.expanded = false;
vm.toggleExpanded = toggleExpanded;
// Panel
vm.resource = resource;
vm.title = $filter('sanitize')(resource.model.get('name'));
// Stdout Navigation
vm.scroll = {
showBackToTop: false,
home: scrollFirst,
end: scrollLast,
down: scrollPageDown,
up: scrollPageUp
};
render.requestAnimationFrame(() => init());
} }
function init () { function bufferAdd (event) {
status.init({ rx.push(event);
resource,
});
page.init({ bufferState[0] += 1;
resource, bufferState[1] += 1;
});
render.init({ return bufferState;
compile: html => $compile(html)($scope), }
isStreamActive: engine.isActive,
});
scroll.init({ function bufferEmpty () {
isAtRest: scrollIsAtRest, bufferState[0] = 0;
previous,
next,
});
engine.init({ return rx.splice(0, rx.length);
page, }
scroll,
resource, function onFrames (events) {
onEventFrame (events) { if (!following) {
return shift().then(() => append(events, true)); const minCounter = Math.min(...events.map(({ counter }) => counter));
}, // attachment range
onStart () { const max = slide.getTailCounter() + 1;
status.setJobStatus('running'); const min = Math.max(1, slide.getHeadCounter(), max - 50);
},
onStop () { if (minCounter > max || minCounter < min) {
stopListening(); return $q.resolve();
status.updateStats();
status.dispatch();
} }
});
streaming = false; follow();
if (status.state.running) {
return scrollLast().then(() => startListening());
} else if (!status.state.finished) {
return scrollFirst().then(() => startListening());
} }
return scrollLast(); const capacity = slide.getCapacity();
if (capacity >= events.length) {
return slide.pushFront(events);
}
delete render.record;
render.record = {};
return slide.popBack(events.length - capacity)
.then(() => slide.pushFront(events))
.then(() => {
scroll.setScrollPosition(scroll.getScrollHeight());
return $q.resolve();
});
}
function first () {
unfollow();
scroll.pause();
return slide.getFirst()
.then(() => {
scroll.resetScrollPosition();
scroll.resume();
return $q.resolve();
});
}
function next () {
return slide.slideDown();
}
function previous () {
unfollow();
const initialPosition = scroll.getScrollPosition();
return slide.slideUp()
.then(changed => {
if (changed[0] !== 0 || changed[1] !== 0) {
const currentHeight = scroll.getScrollHeight();
scroll.setScrollPosition((currentHeight / 4) - initialPosition);
}
return $q.resolve();
});
}
function last () {
scroll.pause();
return slide.getLast()
.then(() => {
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
scroll.setScrollPosition(scroll.getScrollHeight());
scroll.resume();
return $q.resolve();
});
}
function compile (html) {
return $compile(html)($scope);
}
function follow () {
scroll.pause();
scroll.hide();
following = true;
}
function unfollow () {
following = false;
scroll.unhide();
scroll.resume();
}
function showHostDetails (id, uuid) {
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
} }
function stopListening () { function stopListening () {
listeners.forEach(deregister => deregister()); listeners.forEach(deregister => deregister());
listeners = []; listeners.length = 0;
} }
function startListening () { function startListening () {
stopListening(); stopListening();
listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data))); listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data)));
listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data))); listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data)));
} }
@@ -133,271 +160,95 @@ function handleStatusEvent (data) {
} }
function handleJobEvent (data) { function handleJobEvent (data) {
streaming = streaming || attachToRunningJob(); stream.pushJobEvent(data);
status.pushJobEvent(data);
}
streaming.then(() => { function OutputIndexController (
engine.pushJobEvent(data); _$compile_,
status.pushJobEvent(data); _$q_,
_$scope_,
_$state_,
_resource_,
_scroll_,
_render_,
_status_,
_slide_,
_stream_,
$filter,
strings,
) {
$compile = _$compile_;
$q = _$q_;
$scope = _$scope_;
$state = _$state_;
resource = _resource_;
scroll = _scroll_;
render = _render_;
slide = _slide_;
status = _status_;
stream = _stream_;
vm = this || {};
// Panel
vm.strings = strings;
vm.resource = resource;
vm.title = $filter('sanitize')(resource.model.get('name'));
vm.expanded = false;
vm.showHostDetails = showHostDetails;
vm.toggleExpanded = () => { vm.expanded = !vm.expanded; };
// Stdout Navigation
vm.menu = {
end: last,
home: first,
up: previous,
down: next,
};
render.requestAnimationFrame(() => {
bufferInit();
status.init(resource);
slide.init(render, resource.events);
render.init({ compile });
scroll.init({ previous, next });
stream.init({
bufferAdd,
bufferEmpty,
onFrames,
onStop () {
stopListening();
status.updateStats();
status.dispatch();
unfollow();
}
});
startListening();
return last();
}); });
} }
function next () { OutputIndexController.$inject = [
return page.next()
.then(events => {
if (!events) {
return $q.resolve();
}
return shift()
.then(() => append(events))
.then(() => {
if (scroll.isMissing()) {
return next();
}
return $q.resolve();
});
});
}
function previous () {
const initialPosition = scroll.getScrollPosition();
let postPopHeight;
return page.previous()
.then(events => {
if (!events) {
return $q.resolve();
}
return pop()
.then(() => {
postPopHeight = scroll.getScrollHeight();
return prepend(events);
})
.then(() => {
const currentHeight = scroll.getScrollHeight();
scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition);
});
});
}
function append (events, eng) {
return render.append(events)
.then(count => {
page.updateLineCount(count, eng);
});
}
function prepend (events) {
return render.prepend(events)
.then(count => {
page.updateLineCount(count);
});
}
function pop () {
if (!page.isOverCapacity()) {
return $q.resolve();
}
const lines = page.trim();
return render.pop(lines);
}
function shift () {
if (!page.isOverCapacity()) {
return $q.resolve();
}
const lines = page.trim(true);
return render.shift(lines);
}
function scrollFirst () {
if (engine.isActive()) {
if (engine.isTransitioning()) {
return $q.resolve();
}
if (!engine.isPaused()) {
engine.pause(true);
}
} else if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
return page.first()
.then(events => {
if (!events) {
return $q.resolve();
}
return render.clear()
.then(() => prepend(events))
.then(() => {
scroll.resetScrollPosition();
scroll.resume();
})
.then(() => {
if (scroll.isMissing()) {
return next();
}
return $q.resolve();
});
});
}
function scrollLast () {
if (engine.isActive()) {
if (engine.isTransitioning()) {
return $q.resolve();
}
if (engine.isPaused()) {
engine.resume(true);
}
} else if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
return render.clear()
.then(() => page.last())
.then(events => {
if (!events) {
return $q.resolve();
}
const minLine = 1 + Math.max(...events.map(event => event.end_line));
engine.setMinLine(minLine);
return append(events);
})
.then(() => {
if (!engine.isActive()) {
scroll.resume();
}
scroll.setScrollPosition(scroll.getScrollHeight());
})
.then(() => {
if (!engine.isActive() && scroll.isMissing()) {
return previous();
}
return $q.resolve();
});
}
function attachToRunningJob () {
if (engine.isActive()) {
if (engine.isTransitioning()) {
return $q.resolve();
}
if (engine.isPaused()) {
engine.resume(true);
}
} else if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
return page.last()
.then(events => {
if (!events) {
return $q.resolve();
}
const minLine = 1 + Math.max(...events.map(event => event.end_line));
engine.setMinLine(minLine);
return append(events);
})
.then(() => {
scroll.setScrollPosition(scroll.getScrollHeight());
});
}
function scrollPageUp () {
if (scroll.isPaused()) {
return;
}
scroll.pageUp();
}
function scrollPageDown () {
if (scroll.isPaused()) {
return;
}
scroll.pageDown();
}
function scrollIsAtRest (isAtRest) {
vm.scroll.showBackToTop = !isAtRest;
}
function toggleExpanded () {
vm.expanded = !vm.expanded;
}
function devClear () {
render.clear().then(() => init());
}
function showHostDetails (id, uuid) {
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
}
// function toggle (uuid, menu) {
// const lines = $(`.child-of-${uuid}`);
// let icon = $(`#${uuid} .at-Stdout-toggle > i`);
// if (menu || record[uuid].level === 1) {
// vm.isExpanded = !vm.isExpanded;
// }
// if (record[uuid].children) {
// icon = icon.add($(`#${record[uuid].children.join(', #')}`)
// .find('.at-Stdout-toggle > i'));
// }
// if (icon.hasClass('fa-angle-down')) {
// icon.addClass('fa-angle-right');
// icon.removeClass('fa-angle-down');
// lines.addClass('hidden');
// } else {
// icon.addClass('fa-angle-down');
// icon.removeClass('fa-angle-right');
// lines.removeClass('hidden');
// }
// }
JobsIndexController.$inject = [
'$compile', '$compile',
'$filter',
'$q', '$q',
'$scope', '$scope',
'$state', '$state',
'resource', 'resource',
'JobPageService', 'OutputScrollService',
'JobScrollService', 'OutputRenderService',
'JobRenderService', 'OutputStatusService',
'JobEventEngine', 'OutputSlideService',
'JobStatusService', 'OutputStreamService',
'$filter',
'OutputStrings', 'OutputStrings',
]; ];
module.exports = JobsIndexController; module.exports = OutputIndexController;

View File

@@ -3,13 +3,13 @@ import atLibComponents from '~components';
import Strings from '~features/output/output.strings'; import Strings from '~features/output/output.strings';
import Controller from '~features/output/index.controller'; import Controller from '~features/output/index.controller';
import PageService from '~features/output/page.service';
import RenderService from '~features/output/render.service'; import RenderService from '~features/output/render.service';
import ScrollService from '~features/output/scroll.service'; import ScrollService from '~features/output/scroll.service';
import EngineService from '~features/output/engine.service'; import StreamService from '~features/output/stream.service';
import StatusService from '~features/output/status.service'; import StatusService from '~features/output/status.service';
import MessageService from '~features/output/message.service'; import MessageService from '~features/output/message.service';
import EventsApiService from '~features/output/api.events.service'; import EventsApiService from '~features/output/api.events.service';
import SlideService from '~features/output/slide.service';
import LegacyRedirect from '~features/output/legacy.route'; import LegacyRedirect from '~features/output/legacy.route';
import DetailsComponent from '~features/output/details.component'; import DetailsComponent from '~features/output/details.component';
@@ -24,6 +24,8 @@ const MODULE_NAME = 'at.features.output';
const PAGE_CACHE = true; const PAGE_CACHE = true;
const PAGE_LIMIT = 5; const PAGE_LIMIT = 5;
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const ORDER_BY = 'counter';
// const ORDER_BY = 'start_line';
const WS_PREFIX = 'ws'; const WS_PREFIX = 'ws';
function resolveResource ( function resolveResource (
@@ -74,7 +76,7 @@ function resolveResource (
const params = { const params = {
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
order_by: 'start_line', order_by: ORDER_BY,
}; };
const config = { const config = {
@@ -250,13 +252,13 @@ angular
HostEvent HostEvent
]) ])
.service('OutputStrings', Strings) .service('OutputStrings', Strings)
.service('JobPageService', PageService) .service('OutputScrollService', ScrollService)
.service('JobScrollService', ScrollService) .service('OutputRenderService', RenderService)
.service('JobRenderService', RenderService) .service('OutputStreamService', StreamService)
.service('JobEventEngine', EngineService) .service('OutputStatusService', StatusService)
.service('JobStatusService', StatusService) .service('OutputMessageService', MessageService)
.service('JobMessageService', MessageService)
.service('JobEventsApiService', EventsApiService) .service('JobEventsApiService', EventsApiService)
.service('OutputSlideService', SlideService)
.component('atJobSearch', SearchComponent) .component('atJobSearch', SearchComponent)
.component('atJobStats', StatsComponent) .component('atJobStats', StatsComponent)
.component('atJobDetails', DetailsComponent) .component('atJobDetails', DetailsComponent)

View File

@@ -22,17 +22,17 @@
ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }"></i> ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }"></i>
</div> </div>
<div class="pull-right" ng-click="vm.scroll.end()"> <div class="pull-right" ng-click="vm.menu.end()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down" <i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
ng-class=" { 'at-Stdout-menuIcon--active': vm.scroll.isLocked }"></i> ng-class=" { 'at-Stdout-menuIcon--active': vm.menu.isLocked }"></i>
</div> </div>
<div class="pull-right" ng-click="vm.scroll.home()"> <div class="pull-right" ng-click="vm.menu.home()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i> <i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
</div> </div>
<div class="pull-right" ng-click="vm.scroll.down()"> <div class="pull-right" ng-click="vm.menu.down()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i> <i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i>
</div> </div>
<div class="pull-right" ng-click="vm.scroll.up()"> <div class="pull-right" ng-click="vm.menu.up()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i> <i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
</div> </div>
@@ -52,8 +52,8 @@
</table> </table>
</pre> </pre>
<div ng-show="vm.scroll.showBackToTop" class="at-Stdout-menuBottom"> <div ng-show="vm.menu.showBackToTop" class="at-Stdout-menuBottom">
<div class="at-Stdout-menuIconGroup" ng-click="vm.scroll.home()"> <div class="at-Stdout-menuIconGroup" ng-click="vm.menu.home()">
<p class="pull-left"><i class="fa fa-angle-double-up"></i></p> <p class="pull-left"><i class="fa fa-angle-double-up"></i></p>
<p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p> <p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
</div> </div>

View File

@@ -1,283 +0,0 @@
function JobPageService ($q) {
this.init = ({ resource }) => {
this.resource = resource;
this.api = this.resource.events;
this.page = {
limit: this.resource.page.pageLimit,
size: this.resource.page.size,
cache: [],
state: {
count: 0,
current: 0,
first: 0,
last: 0
}
};
this.bookmark = {
pending: false,
set: true,
cache: [],
state: {
count: 0,
first: 0,
last: 0,
current: 0
}
};
this.result = {
limit: this.page.limit * this.page.size,
count: 0
};
this.buffer = {
count: 0
};
};
this.addPage = (number, events, push, reference) => {
const page = { number, events, lines: 0 };
reference = reference || this.getActiveReference();
if (push) {
reference.cache.push(page);
reference.state.last = page.number;
reference.state.first = reference.cache[0].number;
} else {
reference.cache.unshift(page);
reference.state.first = page.number;
reference.state.last = reference.cache[reference.cache.length - 1].number;
}
reference.state.current = page.number;
reference.state.count++;
};
this.addToBuffer = event => {
const reference = this.getReference();
const index = reference.cache.length - 1;
let pageAdded = false;
if (this.result.count % this.page.size === 0) {
this.addPage(reference.state.current + 1, [event], true, reference);
if (this.isBookmarkPending()) {
this.setBookmark();
}
this.trimBuffer();
pageAdded = true;
} else {
reference.cache[index].events.push(event);
}
this.buffer.count++;
this.result.count++;
return pageAdded;
};
this.trimBuffer = () => {
const reference = this.getReference();
const diff = reference.cache.length - this.page.limit;
if (diff <= 0) {
return;
}
for (let i = 0; i < diff; i++) {
if (reference.cache[i].events) {
this.buffer.count -= reference.cache[i].events.length;
reference.cache[i].events.splice(0, reference.cache[i].events.length);
}
}
};
this.isBufferFull = () => {
if (this.buffer.count === 2) {
return true;
}
return false;
};
this.emptyBuffer = () => {
const reference = this.getReference();
let data = [];
for (let i = 0; i < reference.cache.length; i++) {
const count = reference.cache[i].events.length;
if (count > 0) {
this.buffer.count -= count;
data = data.concat(reference.cache[i].events.splice(0, count));
}
}
return data;
};
this.emptyCache = number => {
const reference = this.getActiveReference();
number = number || reference.state.current;
reference.state.first = number;
reference.state.current = number;
reference.state.last = number;
reference.cache.splice(0, reference.cache.length);
};
this.isOverCapacity = () => {
const reference = this.getActiveReference();
return (reference.cache.length - this.page.limit) > 0;
};
this.trim = left => {
const reference = this.getActiveReference();
const excess = reference.cache.length - this.page.limit;
let ejected;
if (left) {
ejected = reference.cache.splice(0, excess);
reference.state.first = reference.cache[0].number;
} else {
ejected = reference.cache.splice(-excess);
reference.state.last = reference.cache[reference.cache.length - 1].number;
}
return ejected.reduce((total, page) => total + page.lines, 0);
};
this.isPageBookmarked = number => number >= this.page.bookmark.first &&
number <= this.page.bookmark.last;
this.updateLineCount = (lines, engine) => {
let reference;
if (engine) {
reference = this.getReference();
} else {
reference = this.getActiveReference();
}
const index = reference.cache.findIndex(item => item.number === reference.state.current);
reference.cache[index].lines += lines;
};
this.isBookmarkPending = () => this.bookmark.pending;
this.isBookmarkSet = () => this.bookmark.set;
this.setBookmark = () => {
if (this.isBookmarkSet()) {
return;
}
if (!this.isBookmarkPending()) {
this.bookmark.pending = true;
return;
}
this.bookmark.state.first = this.page.state.first;
this.bookmark.state.last = this.page.state.last - 1;
this.bookmark.state.current = this.page.state.current - 1;
this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache));
this.bookmark.set = true;
this.bookmark.pending = false;
};
this.removeBookmark = () => {
this.bookmark.set = false;
this.bookmark.pending = false;
this.bookmark.cache.splice(0, this.bookmark.cache.length);
this.bookmark.state.first = 0;
this.bookmark.state.last = 0;
this.bookmark.state.current = 0;
};
this.next = () => {
const reference = this.getActiveReference();
const number = reference.state.last + 1;
return this.api.getPage(number)
.then(data => {
if (!data || !data.results) {
return $q.resolve();
}
this.addPage(data.page, [], true);
return data.results;
});
};
this.previous = () => {
const reference = this.getActiveReference();
return this.api.getPage(reference.state.first - 1)
.then(data => {
if (!data || !data.results) {
return $q.resolve();
}
this.addPage(data.page, [], false);
return data.results;
});
};
this.last = () => this.api.last()
.then(data => {
if (!data || !data.results || !data.results.length > 0) {
return $q.resolve();
}
this.emptyCache(data.page);
this.addPage(data.page, [], true);
return data.results;
});
this.first = () => this.api.first()
.then(data => {
if (!data || !data.results) {
return $q.resolve();
}
this.emptyCache(data.page);
this.addPage(data.page, [], false);
return data.results;
});
this.getActiveReference = () => (this.isBookmarkSet() ?
this.getReference(true) : this.getReference());
this.getReference = (bookmark) => {
if (bookmark) {
return {
bookmark: true,
cache: this.bookmark.cache,
state: this.bookmark.state
};
}
return {
bookmark: false,
cache: this.page.cache,
state: this.page.state
};
};
}
JobPageService.$inject = ['$q'];
export default JobPageService;

View File

@@ -4,7 +4,7 @@ const DELAY = 100;
const THRESHOLD = 0.1; const THRESHOLD = 0.1;
function JobScrollService ($q, $timeout) { function JobScrollService ($q, $timeout) {
this.init = (hooks) => { this.init = ({ next, previous }) => {
this.el = $(ELEMENT_CONTAINER); this.el = $(ELEMENT_CONTAINER);
this.timer = null; this.timer = null;
@@ -14,15 +14,15 @@ function JobScrollService ($q, $timeout) {
}; };
this.hooks = { this.hooks = {
isAtRest: hooks.isAtRest, next,
next: hooks.next, previous,
previous: hooks.previous isAtRest: () => $q.resolve()
}; };
this.state = { this.state = {
locked: false, hidden: false,
paused: false, paused: false,
top: true top: true,
}; };
this.el.scroll(this.listen); this.el.scroll(this.listen);
@@ -158,6 +158,20 @@ function JobScrollService ($q, $timeout) {
this.state.locked = false; this.state.locked = false;
}; };
this.hide = () => {
if (!this.state.hidden) {
this.el.css('overflow', 'hidden');
this.state.hidden = true;
}
};
this.unhide = () => {
if (this.state.hidden) {
this.el.css('overflow', 'auto');
this.state.hidden = false;
}
};
this.isLocked = () => this.state.locked; this.isLocked = () => this.state.locked;
this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight(); this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
} }

View File

@@ -132,7 +132,7 @@ JobSearchController.$inject = [
'$state', '$state',
'QuerySet', 'QuerySet',
'OutputStrings', 'OutputStrings',
'JobStatusService', 'OutputStatusService',
]; ];
export default { export default {

View File

@@ -1,39 +1,37 @@
<!-- todo: styling, css etc. - disposition according to project lib conventions --> <!-- todo: markup, styling, css etc. - disposition according to project lib conventions -->
<!-- todo: further componentization -->
<form ng-submit="vm.submitSearch()"> <form ng-submit="vm.submitSearch()">
<div class="input-group"> <div class="input-group">
<input type="text" <input type="text"
class="form-control at-Input" class="form-control at-Input"
ng-class="{ 'at-Input--rejected': vm.rejected }" ng-disabled="vm.disabled"
ng-model="vm.value" ng-class="{ 'at-Input--rejected': vm.rejected }"
ng-attr-placeholder="{{ vm.running ? vm.strings.get('search.PLACEHOLDER_RUNNING') : ng-model="vm.value"
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}" ng-attr-placeholder="{{ vm.running ?
ng-disabled="vm.disabled"> vm.strings.get('search.PLACEHOLDER_RUNNING') :
<span class="input-group-btn"> vm.strings.get('search.PLACEHOLDER_DEFAULT') }}">
<button class="btn at-ButtonHollow--default at-Input-button" <span class="input-group-btn">
<button class="btn at-ButtonHollow--default at-Input-button"
ng-click="vm.submitSearch()" ng-click="vm.submitSearch()"
ng-disabled="vm.disabled" ng-disabled="vm.disabled"
type="button"> type="button">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
<button class="btn jobz-Button-searchKey" <button class="btn jobz-Button-searchKey"
ng-if="vm.key" ng-if="vm.key"
ng-disabled="vm.disabled" ng-disabled="vm.disabled"
ng-click="vm.toggleSearchKey()" ng-click="vm.toggleSearchKey()"
type="button"> type="button"> {{:: vm.strings.get('search.KEY') }}
{{:: vm.strings.get('search.KEY') }} </button>
</button> <button class="btn at-ButtonHollow--default at-Input-button"
<button class="btn at-ButtonHollow--default at-Input-button"
ng-if="!vm.key" ng-if="!vm.key"
ng-disabled="vm.disabled" ng-disabled="vm.disabled"
ng-click="vm.toggleSearchKey()" ng-click="vm.toggleSearchKey()"
type="button"> type="button"> {{:: vm.strings.get('search.KEY') }}
{{:: vm.strings.get('search.KEY') }} </button>
</button> </span>
</span> </div>
</div> <p ng-if="vm.rejected" class="at-InputMessage--rejected">{{ vm.message }}</p>
<p ng-if="vm.rejected" class="at-InputMessage--rejected">
{{ vm.message }}
</p>
</form> </form>
<div class="jobz-tagz"> <div class="jobz-tagz">
@@ -41,19 +39,25 @@
<at-tag tag="tag" remove-tag="vm.removeSearchTag($index)"></at-tag> <at-tag tag="tag" remove-tag="vm.removeSearchTag($index)"></at-tag>
</div> </div>
<div class="jobz-searchClearAllContainer"> <div class="jobz-searchClearAllContainer">
<a href class="jobz-searchClearAll" ng-click="vm.clearSearch()" ng-show="!(vm.tags | isEmpty)">{{:: vm.strings.get('search.CLEAR_ALL') }}</a> <a class="jobz-searchClearAll"
ng-click="vm.clearSearch()"
ng-show="!(vm.tags | isEmpty)"
href> {{:: vm.strings.get('search.CLEAR_ALL') }}
</a>
</div> </div>
</div> </div>
<div class="jobz-searchKeyPaneContainer" ng-show="vm.key"> <div class="jobz-searchKeyPaneContainer" ng-show="vm.key">
<div class="jobz-searchKeyPane"> <div class="jobz-searchKeyPane">
<div class="SmartSearch-keyRow"> <div class="SmartSearch-keyRow">
<div class="SmartSearch-examples"> <div class="SmartSearch-examples">
<div class="SmartSearch-examples--title"> <div class="SmartSearch-examples--title">
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b> <b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
</div>
<div class="SmartSearch-examples--search"
ng-repeat="tag in vm.examples">{{ tag }}
</div>
</div> </div>
<div class="SmartSearch-examples--search" ng-repeat="tag in vm.examples">{{ tag }}</div>
</div>
</div> </div>
<div class="SmartSearch-keyRow"> <div class="SmartSearch-keyRow">
<b>{{:: vm.strings.get('search.FIELDS') }}:</b> <b>{{:: vm.strings.get('search.FIELDS') }}:</b>

View File

@@ -0,0 +1,298 @@
/* eslint camelcase: 0 */
const PAGE_SIZE = 50;
const PAGE_LIMIT = 5;
const EVENT_LIMIT = PAGE_LIMIT * PAGE_SIZE;
const TAIL_ADDITION = 'TAIL_ADDITION';
const TAIL_DELETION = 'TAIL_DELETION';
const HEAD_ADDITION = 'HEAD_ADDITION';
const HEAD_DELETION = 'HEAD_DELETION';
function SlidingWindowService ($q) {
this.init = (storage, api) => {
const { prepend, append, shift, pop } = storage;
const { getMaxCounter, getRange, getFirst, getLast } = api;
this.api = {
getMaxCounter,
getRange,
getFirst,
getLast
};
this.storage = {
prepend,
append,
shift,
pop
};
this.commands = {
[TAIL_ADDITION]: this.pushFront,
[HEAD_ADDITION]: this.pushBack,
[TAIL_DELETION]: this.popFront,
[HEAD_DELETION]: this.popBack
};
this.vectors = {
[TAIL_ADDITION]: [0, 1],
[HEAD_ADDITION]: [-1, 0],
[TAIL_DELETION]: [0, -1],
[HEAD_DELETION]: [1, 0],
};
this.records = {};
this.chain = $q.resolve();
};
this.pushFront = events => {
const tail = this.getTailCounter();
const newEvents = events.filter(({ counter }) => counter > tail);
return this.storage.append(newEvents)
.then(() => {
newEvents.forEach(({ counter, start_line, end_line }) => {
this.records[counter] = { start_line, end_line };
});
return $q.resolve();
});
};
this.pushBack = events => {
const [head, tail] = this.getRange();
const newEvents = events
.filter(({ counter }) => counter < head || counter > tail);
return this.storage.prepend(newEvents)
.then(() => {
newEvents.forEach(({ counter, start_line, end_line }) => {
this.records[counter] = { start_line, end_line };
});
return $q.resolve();
});
};
this.popFront = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const max = this.getTailCounter();
const min = Math.max(this.getHeadCounter(), max - count);
let lines = 0;
for (let i = min; i <= max; ++i) {
if (this.records[i]) {
lines += (this.records[i].end_line - this.records[i].start_line);
}
}
return this.storage.pop(lines)
.then(() => {
for (let i = min; i <= max; ++i) {
delete this.records[i];
}
return $q.resolve();
});
};
this.popBack = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const min = this.getHeadCounter();
const max = Math.min(this.getTailCounter(), min + count);
let lines = 0;
for (let i = min; i <= max; ++i) {
if (this.records[i]) {
lines += (this.records[i].end_line - this.records[i].start_line);
}
}
return this.storage.shift(lines)
.then(() => {
for (let i = min; i <= max; ++i) {
delete this.records[i];
}
return $q.resolve();
});
};
this.getBoundedRange = ([low, high]) => {
const bounds = [1, this.getMaxCounter()];
return [Math.max(low, bounds[0]), Math.min(high, bounds[1])];
};
this.move = ([low, high]) => {
const [head, tail] = this.getRange();
const [newHead, newTail] = this.getBoundedRange([low, high]);
if (newHead > newTail) {
return $q.resolve([0, 0]);
}
if (!Number.isFinite(newHead) || !Number.isFinite(newTail)) {
return $q.resolve([0, 0]);
}
const additions = [];
const deletions = [];
for (let counter = tail + 1; counter <= newTail; counter++) {
additions.push([counter, TAIL_ADDITION]);
}
for (let counter = head - 1; counter >= newHead; counter--) {
additions.push([counter, HEAD_ADDITION]);
}
for (let counter = head; counter < newHead; counter++) {
deletions.push([counter, HEAD_DELETION]);
}
for (let counter = tail; counter > newTail; counter--) {
deletions.push([counter, TAIL_DELETION]);
}
const hasCounter = (items, n) => items
.filter(([counter]) => counter === n).length !== 0;
const commandRange = {
[TAIL_DELETION]: 0,
[HEAD_DELETION]: 0,
[TAIL_ADDITION]: [tail, tail],
[HEAD_ADDITION]: [head, head],
};
deletions.forEach(([counter, key]) => {
if (!hasCounter(additions, counter)) {
commandRange[key] += 1;
}
commandRange[TAIL_ADDITION][0] += this.vectors[key][0];
commandRange[TAIL_ADDITION][1] += this.vectors[key][1];
commandRange[HEAD_ADDITION][0] += this.vectors[key][0];
commandRange[HEAD_ADDITION][1] += this.vectors[key][1];
});
additions.forEach(([counter, key]) => {
if (!hasCounter(deletions, counter)) {
if (counter < commandRange[key][0]) {
commandRange[key][0] = counter;
}
if (counter > commandRange[key][1]) {
commandRange[key][1] = counter;
}
}
});
this.chain = this.chain
.then(() => this.commands[TAIL_DELETION](commandRange[TAIL_DELETION]))
.then(() => this.commands[HEAD_DELETION](commandRange[HEAD_DELETION]))
.then(() => this.api.getRange(commandRange[TAIL_ADDITION]))
.then(events => this.commands[TAIL_ADDITION](events))
.then(() => this.api.getRange(commandRange[HEAD_ADDITION]))
.then(events => this.commands[HEAD_ADDITION](events))
.then(() => {
const range = this.getRange();
const displacement = [range[0] - head, range[1] - tail];
return $q.resolve(displacement);
});
return this.chain;
};
this.slideDown = (displacement = PAGE_SIZE) => {
const [head, tail] = this.getRange();
const tailRoom = this.getMaxCounter() - tail;
const tailDisplacement = Math.min(tailRoom, displacement);
const headDisplacement = Math.min(tailRoom, displacement);
return this.move([head + headDisplacement, tail + tailDisplacement]);
};
this.slideUp = (displacement = PAGE_SIZE) => {
const [head, tail] = this.getRange();
const headRoom = head - 1;
const headDisplacement = Math.min(headRoom, displacement);
const tailDisplacement = Math.min(headRoom, displacement);
return this.move([head - headDisplacement, tail - tailDisplacement]);
};
this.moveHead = displacement => {
const [head, tail] = this.getRange();
const headRoom = head - 1;
const headDisplacement = Math.min(headRoom, displacement);
return this.move([head + headDisplacement, tail]);
};
this.moveTail = displacement => {
const [head, tail] = this.getRange();
const tailRoom = this.getMaxCounter() - tail;
const tailDisplacement = Math.max(tailRoom, displacement);
return this.move([head, tail + tailDisplacement]);
};
this.clear = () => {
const count = this.getRecordCount();
if (count > 0) {
this.chain = this.chain
.then(() => this.commands[HEAD_DELETION](count));
}
return this.chain;
};
this.getFirst = () => this.clear()
.then(() => this.api.getFirst())
.then(events => this.commands[TAIL_ADDITION](events))
.then(() => this.moveTail(PAGE_SIZE));
this.getLast = () => this.clear()
.then(() => this.api.getLast())
.then(events => this.commands[HEAD_ADDITION](events))
.then(() => this.moveHead(-PAGE_SIZE));
this.getTailCounter = () => {
const tail = Math.max(...Object.keys(this.records));
return Number.isFinite(tail) ? tail : 0;
};
this.getHeadCounter = () => {
const head = Math.min(...Object.keys(this.records));
return Number.isFinite(head) ? head : 0;
};
this.compareRange = (a, b) => a[0] === b[0] && a[1] === b[1];
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()];
this.getMaxCounter = () => this.api.getMaxCounter();
this.getRecordCount = () => Object.keys(this.records).length;
this.getCapacity = () => EVENT_LIMIT - this.getRecordCount();
}
SlidingWindowService.$inject = ['$q'];
export default SlidingWindowService;

View File

@@ -63,7 +63,7 @@ function JobStatsController (strings, { subscribe }) {
JobStatsController.$inject = [ JobStatsController.$inject = [
'OutputStrings', 'OutputStrings',
'JobStatusService', 'OutputStatusService',
]; ];
export default { export default {

View File

@@ -1,4 +1,4 @@
<!-- todo: styling, css etc. - disposition according to project lib conventions --> <!-- todo: styling, markup, css etc. - disposition according to project lib conventions -->
<div class="at-u-floatRight"> <div class="at-u-floatRight">
<span class="at-Panel-label">plays</span> <span class="at-Panel-label">plays</span>
<span ng-show="!vm.plays" class="at-Panel-headingTitleBadge">...</span> <span ng-show="!vm.plays" class="at-Panel-headingTitleBadge">...</span>

View File

@@ -12,9 +12,7 @@ function JobStatusService (moment, message) {
this.dispatch = () => message.dispatch('status', this.state); this.dispatch = () => message.dispatch('status', this.state);
this.subscribe = listener => message.subscribe('status', listener); this.subscribe = listener => message.subscribe('status', listener);
this.init = ({ resource }) => { this.init = ({ model, stats }) => {
const { model } = resource;
this.created = model.get('created'); this.created = model.get('created');
this.job = model.get('id'); this.job = model.get('id');
this.jobType = model.get('type'); this.jobType = model.get('type');
@@ -43,7 +41,7 @@ function JobStatusService (moment, message) {
}, },
}; };
this.setStatsEvent(resource.stats); this.setStatsEvent(stats);
this.updateStats(); this.updateStats();
this.updateRunningState(); this.updateRunningState();
@@ -213,7 +211,7 @@ function JobStatusService (moment, message) {
JobStatusService.$inject = [ JobStatusService.$inject = [
'moment', 'moment',
'JobMessageService', 'OutputMessageService',
]; ];
export default JobStatusService; export default JobStatusService;

View File

@@ -0,0 +1,146 @@
/* eslint camelcase: 0 */
const PAGE_SIZE = 50;
const MAX_LAG = 120;
const JOB_END = 'playbook_on_stats';
function OutputStream ($q) {
this.init = ({ bufferAdd, bufferEmpty, onFrames, onStop }) => {
this.hooks = {
bufferAdd,
bufferEmpty,
onFrames,
onStop,
};
this.counters = {
used: [],
min: 1,
max: 0,
ready: false,
};
this.state = {
ending: false,
ended: false
};
this.lag = 0;
this.chain = $q.resolve();
this.factors = this.calcFactors(PAGE_SIZE);
this.setFramesPerRender();
};
this.calcFactors = size => {
const factors = [1];
for (let i = 2; i <= size / 2; i++) {
if (size % i === 0) {
factors.push(i);
}
}
factors.push(size);
return factors;
};
this.setFramesPerRender = () => {
const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
const boundedIndex = Math.min(this.factors.length - 1, index);
this.framesPerRender = this.factors[boundedIndex];
};
this.setMissingCounterThreshold = counter => {
if (counter > this.counters.min) {
this.counters.min = counter;
}
};
this.updateCounterState = ({ counter }) => {
this.counters.used.push(counter);
if (counter > this.counters.max) {
this.counters.max = counter;
}
const missing = [];
const ready = [];
for (let i = this.counters.min; i < this.counters.max; i++) {
if (this.counters.used.indexOf(i) === -1) {
missing.push(i);
} else if (missing.length === 0) {
ready.push(i);
}
}
if (missing.length === 0) {
this.counters.ready = true;
this.counters.min = this.counters.max + 1;
this.counters.used = [];
} else {
this.counters.ready = false;
}
this.counters.missing = missing;
this.counters.readyLines = ready;
return this.counters.ready;
};
this.pushJobEvent = data => {
this.lag++;
this.chain = this.chain
.then(() => {
if (data.event === JOB_END) {
this.state.ending = true;
}
const isMissingCounters = !this.updateCounterState(data);
const [length, count] = this.hooks.bufferAdd(data);
if (count % PAGE_SIZE === 0) {
this.setFramesPerRender();
}
const isBatchReady = length % this.framesPerRender === 0;
const isReady = this.state.ended || (!isMissingCounters && isBatchReady);
if (!isReady) {
return $q.resolve();
}
const events = this.hooks.bufferEmpty();
return this.emitFrames(events);
})
.then(() => --this.lag);
return this.chain;
};
this.emitFrames = events => this.hooks.onFrames(events)
.then(() => {
if (this.state.ending) {
const lastEvents = this.hooks.bufferEmpty();
if (lastEvents.length) {
return this.emitFrames(lastEvents);
}
this.state.ending = false;
this.state.ended = true;
this.hooks.onStop();
}
return $q.resolve();
});
}
OutputStream.$inject = ['$q'];
export default OutputStream;