Merge pull request #2057 from jakemcdermott/job-results/slide

fix memory leak in output render service, rewrite output data storage
This commit is contained in:
Jake McDermott 2018-06-08 03:10:25 -04:00 committed by GitHub
commit f78f179789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 848 additions and 1070 deletions

View File

@ -1,144 +1,94 @@
const PAGE_LIMIT = 5;
const API_PAGE_SIZE = 200;
const PAGE_SIZE = 50;
const ORDER_BY = 'counter';
const BASE_PARAMS = {
order_by: 'start_line',
page_size: PAGE_SIZE,
order_by: ORDER_BY,
};
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) {
this.init = (endpoint, params) => {
this.keys = [];
this.cache = {};
this.pageSizes = {};
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.getFirst = () => {
const page = 1;
const params = merge(this.params, { page });
this.cache = {};
this.keys = [];
this.pageSizes = {};
};
this.fetch = () => this.first().then(() => this);
this.getPage = number => {
if (number < 1 || number > this.state.last) {
return $q.resolve();
}
if (this.cache[number]) {
if (this.pageSizes[number] === PAGE_SIZE) {
return this.cache[number];
}
delete this.pageSizes[number];
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 })
return $http.get(this.endpoint, { params })
.then(({ data }) => {
const { results, count } = data;
this.state.results = results;
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.state.current = page;
this.pageSizes[number] = results.length;
return { results, page: number };
return results;
});
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.next = () => this.getPage(this.state.next);
this.previous = () => this.getPage(this.state.previous);
this.getRange = range => {
if (!range) {
return $q.resolve([]);
}
this.last = () => {
const params = merge({}, this.state.params);
const [low, high] = range;
const params = merge(this.params, { counter__gte: [low], counter__lte: [high] });
delete params.page;
delete params.order_by;
params.page_size = API_PAGE_SIZE;
params.page = 1;
params.order_by = '-start_line';
const promise = $http.get(this.endpoint, { params })
return $http.get(this.endpoint, { params })
.then(({ data }) => {
const { results, count } = data;
const lastPage = this.getLastPage(count);
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) {
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.page = lastPage;
this.state.next = lastPage;
this.state.last = lastPage;
this.state.previous = Math.max(1, this.state.page - 1);
this.state.current = Math.ceil(count / PAGE_SIZE);
this.clearCache();
return { results, page: lastPage };
return rotated;
});
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 = [
'$http',
'$q'
];
JobEventsApiService.$inject = ['$http', '$q'];
export default JobEventsApiService;

View File

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

View File

@ -1,4 +1,5 @@
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
<!-- todo: further componentization -->
<div class="JobResults-panelHeader">
<div class="JobResults-panelHeaderText"> {{:: vm.strings.get('details.HEADER')}}</div>
<!-- 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,126 +1,156 @@
/* eslint camelcase: 0 */
let $compile;
let $filter;
let $q;
let $scope;
let $state;
let page;
let render;
let resource;
let render;
let scroll;
let engine;
let status;
let slide;
let stream;
let vm;
let streaming;
let listeners = [];
function JobsIndexController (
_$compile_,
_$filter_,
_$q_,
_$scope_,
_resource_,
_page_,
_scroll_,
_render_,
_engine_,
_status_,
_strings_,
) {
vm = this || {};
const bufferState = [0, 0]; // [length, count]
const listeners = [];
const rx = [];
$compile = _$compile_;
$filter = _$filter_;
$q = _$q_;
$scope = _$scope_;
let following = false;
resource = _resource_;
page = _page_;
scroll = _scroll_;
render = _render_;
engine = _engine_;
status = _status_;
function bufferInit () {
rx.length = 0;
vm.strings = _strings_;
// 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());
bufferState[0] = 0;
bufferState[1] = 0;
}
function init () {
status.init({
resource,
});
function bufferAdd (event) {
rx.push(event);
page.init({
resource,
});
bufferState[0] += 1;
bufferState[1] += 1;
render.init({
compile: html => $compile(html)($scope),
isStreamActive: engine.isActive,
});
return bufferState;
}
scroll.init({
isAtRest: scrollIsAtRest,
previous,
next,
});
function bufferEmpty () {
bufferState[0] = 0;
engine.init({
page,
scroll,
resource,
onEventFrame (events) {
return shift().then(() => append(events, true));
},
onStart () {
status.setJobStatus('running');
},
onStop () {
stopListening();
status.updateStats();
status.dispatch();
return rx.splice(0, rx.length);
}
function onFrames (events) {
if (!following) {
const minCounter = Math.min(...events.map(({ counter }) => counter));
// attachment range
const max = slide.getTailCounter() + 1;
const min = Math.max(1, slide.getHeadCounter(), max - 50);
if (minCounter > max || minCounter < min) {
return $q.resolve();
}
});
streaming = false;
if (status.state.running) {
return scrollLast().then(() => startListening());
} else if (!status.state.finished) {
return scrollFirst().then(() => startListening());
follow();
}
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 () {
listeners.forEach(deregister => deregister());
listeners = [];
listeners.length = 0;
}
function startListening () {
stopListening();
listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data)));
listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data)));
}
@ -130,280 +160,95 @@ function handleStatusEvent (data) {
}
function handleJobEvent (data) {
streaming = streaming || attachToRunningJob();
stream.pushJobEvent(data);
status.pushJobEvent(data);
}
streaming.then(() => {
engine.pushJobEvent(data);
status.pushJobEvent(data);
function OutputIndexController (
_$compile_,
_$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 () {
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) {
// jobEvent.request('get', id)
// .then(() => {
// const title = jobEvent.get('host_name');
// vm.host = {
// menu: true,
// stdout: jobEvent.get('stdout')
// };
// $scope.jobs.modal.show(title);
// });
// }
// 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 = [
OutputIndexController.$inject = [
'$compile',
'$filter',
'$q',
'$scope',
'$state',
'resource',
'JobPageService',
'JobScrollService',
'JobRenderService',
'JobEventEngine',
'JobStatusService',
'OutputScrollService',
'OutputRenderService',
'OutputStatusService',
'OutputSlideService',
'OutputStreamService',
'$filter',
'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 Controller from '~features/output/index.controller';
import PageService from '~features/output/page.service';
import RenderService from '~features/output/render.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 MessageService from '~features/output/message.service';
import EventsApiService from '~features/output/api.events.service';
import SlideService from '~features/output/slide.service';
import LegacyRedirect from '~features/output/legacy.route';
import DetailsComponent from '~features/output/details.component';
@ -24,6 +24,7 @@ const MODULE_NAME = 'at.features.output';
const PAGE_CACHE = true;
const PAGE_LIMIT = 5;
const PAGE_SIZE = 50;
const ORDER_BY = 'counter';
const WS_PREFIX = 'ws';
function resolveResource (
@ -74,7 +75,7 @@ function resolveResource (
const params = {
page_size: PAGE_SIZE,
order_by: 'start_line',
order_by: ORDER_BY,
};
const config = {
@ -250,13 +251,13 @@ angular
HostEvent
])
.service('OutputStrings', Strings)
.service('JobPageService', PageService)
.service('JobScrollService', ScrollService)
.service('JobRenderService', RenderService)
.service('JobEventEngine', EngineService)
.service('JobStatusService', StatusService)
.service('JobMessageService', MessageService)
.service('OutputScrollService', ScrollService)
.service('OutputRenderService', RenderService)
.service('OutputStreamService', StreamService)
.service('OutputStatusService', StatusService)
.service('OutputMessageService', MessageService)
.service('JobEventsApiService', EventsApiService)
.service('OutputSlideService', SlideService)
.component('atJobSearch', SearchComponent)
.component('atJobStats', StatsComponent)
.component('atJobDetails', DetailsComponent)

View File

@ -22,17 +22,17 @@
ng-class="{ 'fa-minus': vm.expanded, 'fa-plus': !vm.expanded }"></i>
</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"
ng-class=" { 'at-Stdout-menuIcon--active': vm.scroll.isLocked }"></i>
ng-class=" { 'at-Stdout-menuIcon--active': vm.menu.isLocked }"></i>
</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>
</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>
</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>
</div>
@ -52,8 +52,8 @@
</table>
</pre>
<div ng-show="vm.scroll.showBackToTop" class="at-Stdout-menuBottom">
<div class="at-Stdout-menuIconGroup" ng-click="vm.scroll.home()">
<div ng-show="vm.menu.showBackToTop" class="at-Stdout-menuBottom">
<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-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
</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

@ -30,11 +30,13 @@ const re = new RegExp(pattern);
const hasAnsi = input => re.test(input);
function JobRenderService ($q, $sce, $window) {
this.init = ({ compile, isStreamActive }) => {
this.init = ({ compile }) => {
this.parent = null;
this.record = {};
this.el = $(ELEMENT_TBODY);
this.hooks = { isStreamActive, compile };
this.hooks = { compile };
this.createToggles = false;
};
this.sortByLineNumber = (a, b) => {
@ -55,12 +57,11 @@ function JobRenderService ($q, $sce, $window) {
events.sort(this.sortByLineNumber);
events.forEach(event => {
const line = this.transformEvent(event);
for (let i = 0; i < events.length; ++i) {
const line = this.transformEvent(events[i]);
html += line.html;
lines += line.count;
});
}
return { html, lines };
};
@ -177,13 +178,13 @@ function JobRenderService ($q, $sce, $window) {
}
if (current) {
if (!this.hooks.isStreamActive() && current.isParent && current.line === ln) {
if (this.createToggles && current.isParent && current.line === ln) {
id = current.uuid;
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
}
if (current.isHost) {
tdEvent = `<td class="at-Stdout-event--host" ui-sref="output.host-event.json({eventId: ${current.id}, taskUuid: '${current.uuid}' })"><span ng-non-bindable>${content}</span></td>`;
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}', '${current.uuid}')">${content}</td>`;
}
if (current.time && current.line === ln) {
@ -239,18 +240,7 @@ function JobRenderService ($q, $sce, $window) {
return list;
};
this.insert = (events, insert) => {
const result = this.transformEventGroup(events);
const html = this.trustHtml(result.html);
return this.requestAnimationFrame(() => insert(html))
.then(() => this.compile(html))
.then(() => result.lines);
};
this.remove = elements => this.requestAnimationFrame(() => {
elements.remove();
});
this.remove = elements => this.requestAnimationFrame(() => elements.remove());
this.requestAnimationFrame = fn => $q(resolve => {
$window.requestAnimationFrame(() => {
@ -262,9 +252,8 @@ function JobRenderService ($q, $sce, $window) {
});
});
this.compile = html => {
html = $(this.el);
this.hooks.compile(html);
this.compile = content => {
this.hooks.compile(content);
return this.requestAnimationFrame();
};
@ -286,9 +275,35 @@ function JobRenderService ($q, $sce, $window) {
return this.remove(elements);
};
this.prepend = events => this.insert(events, html => this.el.prepend(html));
this.prepend = events => {
if (events.length < 1) {
return $q.resolve();
}
this.append = events => this.insert(events, html => this.el.append(html));
const result = this.transformEventGroup(events);
const html = this.trustHtml(result.html);
const newElements = angular.element(html);
return this.requestAnimationFrame(() => this.el.prepend(newElements))
.then(() => this.compile(newElements))
.then(() => result.lines);
};
this.append = events => {
if (events.length < 1) {
return $q.resolve();
}
const result = this.transformEventGroup(events);
const html = this.trustHtml(result.html);
const newElements = angular.element(html);
return this.requestAnimationFrame(() => this.el.append(newElements))
.then(() => this.compile(newElements))
.then(() => result.lines);
};
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));

View File

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

View File

@ -132,7 +132,7 @@ JobSearchController.$inject = [
'$state',
'QuerySet',
'OutputStrings',
'JobStatusService',
'OutputStatusService',
];
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()">
<div class="input-group">
<input type="text"
class="form-control at-Input"
ng-class="{ 'at-Input--rejected': vm.rejected }"
ng-model="vm.value"
ng-attr-placeholder="{{ vm.running ? vm.strings.get('search.PLACEHOLDER_RUNNING') :
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}"
ng-disabled="vm.disabled">
<span class="input-group-btn">
<button class="btn at-ButtonHollow--default at-Input-button"
<div class="input-group">
<input type="text"
class="form-control at-Input"
ng-disabled="vm.disabled"
ng-class="{ 'at-Input--rejected': vm.rejected }"
ng-model="vm.value"
ng-attr-placeholder="{{ vm.running ?
vm.strings.get('search.PLACEHOLDER_RUNNING') :
vm.strings.get('search.PLACEHOLDER_DEFAULT') }}">
<span class="input-group-btn">
<button class="btn at-ButtonHollow--default at-Input-button"
ng-click="vm.submitSearch()"
ng-disabled="vm.disabled"
type="button">
<i class="fa fa-search"></i>
</button>
<button class="btn jobz-Button-searchKey"
<i class="fa fa-search"></i>
</button>
<button class="btn jobz-Button-searchKey"
ng-if="vm.key"
ng-disabled="vm.disabled"
ng-click="vm.toggleSearchKey()"
type="button">
{{:: vm.strings.get('search.KEY') }}
</button>
<button class="btn at-ButtonHollow--default at-Input-button"
type="button"> {{:: vm.strings.get('search.KEY') }}
</button>
<button class="btn at-ButtonHollow--default at-Input-button"
ng-if="!vm.key"
ng-disabled="vm.disabled"
ng-click="vm.toggleSearchKey()"
type="button">
{{:: vm.strings.get('search.KEY') }}
</button>
</span>
</div>
<p ng-if="vm.rejected" class="at-InputMessage--rejected">
{{ vm.message }}
</p>
type="button"> {{:: vm.strings.get('search.KEY') }}
</button>
</span>
</div>
<p ng-if="vm.rejected" class="at-InputMessage--rejected">{{ vm.message }}</p>
</form>
<div class="jobz-tagz">
@ -41,19 +39,25 @@
<at-tag tag="tag" remove-tag="vm.removeSearchTag($index)"></at-tag>
</div>
<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 class="jobz-searchKeyPaneContainer" ng-show="vm.key">
<div class="jobz-searchKeyPane">
<div class="SmartSearch-keyRow">
<div class="SmartSearch-examples">
<div class="SmartSearch-examples--title">
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
<div class="SmartSearch-examples">
<div class="SmartSearch-examples--title">
<b>{{:: vm.strings.get('search.EXAMPLES') }}:</b>
</div>
<div class="SmartSearch-examples--search"
ng-repeat="tag in vm.examples">{{ tag }}
</div>
</div>
<div class="SmartSearch-examples--search" ng-repeat="tag in vm.examples">{{ tag }}</div>
</div>
</div>
<div class="SmartSearch-keyRow">
<b>{{:: vm.strings.get('search.FIELDS') }}:</b>

View File

@ -0,0 +1,322 @@
/* eslint camelcase: 0 */
const PAGE_SIZE = 50;
const PAGE_LIMIT = 5;
const EVENT_LIMIT = PAGE_LIMIT * PAGE_SIZE;
/**
* Check if a range overlaps another range
*
* @arg {Array} range - A [low, high] range array.
* @arg {Array} other - A [low, high] range array to be compared with the first.
*
* @returns {Boolean} - Indicating that the ranges overlap.
*/
function checkRangeOverlap (range, other) {
const span = Math.max(range[1], other[1]) - Math.min(range[0], other[0]);
return (range[1] - range[0]) + (other[1] - other[0]) >= span;
}
/**
* Get an array that describes the overlap of two ranges.
*
* @arg {Array} range - A [low, high] range array.
* @arg {Array} other - A [low, high] range array to be compared with the first.
*
* @returns {(Array|Boolean)} - Returns false if the ranges aren't overlapping.
* For overlapping ranges, a length-2 array describing the nature of the overlap
* is returned. The overlap array describes the position of the second range in
* terms of how many steps inward (negative) or outward (positive) its sides are
* relative to the first range.
*
* ++45678
* 234---- => getOverlapArray([4, 8], [2, 4]) = [2, -4]
*
* 45678
* 45--- => getOverlapArray([4, 8], [4, 5]) = [0, -3]
*
* 45678
* -56-- => getOverlapArray([4, 8], [5, 6]) = [-1, -2]
*
* 45678
* --678 => getOverlapArray([4, 8], [6, 8]) = [-2, 0]
*
* 456++
* --678 => getOverlapArray([4, 6], [6, 8]) = [-2, 2]
*
* +++456++
* 12345678 => getOverlapArray([4, 6], [1, 8]) = [3, 2]
^
* 12345678
* ---456-- => getOverlapArray([1, 8], [4, 6]) = [-3, -2]
*/
function getOverlapArray (range, other) {
if (!checkRangeOverlap(range, other)) {
return false;
}
return [range[0] - other[0], other[1] - range[1]];
}
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.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 = max - count;
let lines = 0;
for (let i = max; i >= min; --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 = max; i >= min; --i) {
delete this.records[i];
}
return $q.resolve();
});
};
this.popBack = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const min = this.getHeadCounter();
const max = min + count;
let lines = 0;
for (let i = min; i <= min + count; ++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 overlap = getOverlapArray([head, tail], [newHead, newTail]);
if (!overlap) {
this.chain = this.chain
.then(() => this.popBack(this.getRecordCount()))
.then(() => this.api.getRange([newHead, newTail]))
.then(events => this.pushFront(events));
}
if (overlap && overlap[0] > 0) {
const pushBackRange = [head - overlap[0], head];
this.chain = this.chain
.then(() => this.api.getRange(pushBackRange))
.then(events => this.pushBack(events));
}
if (overlap && overlap[1] > 0) {
const pushFrontRange = [tail, tail + overlap[1]];
this.chain = this.chain
.then(() => this.api.getRange(pushFrontRange))
.then(events => this.pushFront(events));
}
if (overlap && overlap[0] < 0) {
this.chain = this.chain.then(() => this.popBack(Math.abs(overlap[0])));
}
if (overlap && overlap[1] < 0) {
this.chain = this.chain.then(() => this.popFront(Math.abs(overlap[1])));
}
this.chain = this.chain
.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 newTail = tail + tailDisplacement;
let headDisplacement = 0;
if (newTail - head > EVENT_LIMIT) {
headDisplacement = (newTail - EVENT_LIMIT) - head;
}
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 newHead = head - headDisplacement;
let tailDisplacement = 0;
if (tail - newHead > EVENT_LIMIT) {
tailDisplacement = tail - (newHead + EVENT_LIMIT);
}
return this.move([newHead, 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.popBack(count));
}
return this.chain;
};
this.getFirst = () => this.clear()
.then(() => this.api.getFirst())
.then(events => this.pushFront(events))
.then(() => this.moveTail(PAGE_SIZE));
this.getLast = () => this.clear()
.then(() => this.api.getLast())
.then(events => this.pushBack(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 = [
'OutputStrings',
'JobStatusService',
'OutputStatusService',
];
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">
<span class="at-Panel-label">plays</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.subscribe = listener => message.subscribe('status', listener);
this.init = ({ resource }) => {
const { model } = resource;
this.init = ({ model, stats }) => {
this.created = model.get('created');
this.job = model.get('id');
this.jobType = model.get('type');
@ -43,7 +41,7 @@ function JobStatusService (moment, message) {
},
};
this.setStatsEvent(resource.stats);
this.setStatsEvent(stats);
this.updateStats();
this.updateRunningState();
@ -213,7 +211,7 @@ function JobStatusService (moment, message) {
JobStatusService.$inject = [
'moment',
'JobMessageService',
'OutputMessageService',
];
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;