blob: 9a47b1b838c097836fef2e64e15d2101b2d7637d [file] [log] [blame]
#
# Copyright (C) 2017 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""UI classes for build output."""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import division
import math
import os
import sys
import time
import ndk.ansi
class UiRenderer(object):
def __init__(self, console):
self.console = console
def clear_last_render(self):
raise NotImplementedError
def render(self, lines):
raise NotImplementedError
class AnsiUiRenderer(UiRenderer):
# Number of seconds to delay between each draw command when debugging.
debug_draw_delay = 0.1
def __init__(self, console, debug_draw=False):
super(AnsiUiRenderer, self).__init__(console)
self.last_rendered_lines = []
self.debug_draw = debug_draw
def changed_lines(self, new_lines):
assert len(new_lines) == len(self.last_rendered_lines)
old_lines = self.last_rendered_lines
for idx, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
if old_line != new_line:
yield idx, new_line
def clear_last_render(self):
self.console.clear_lines(len(self.last_rendered_lines))
self.last_rendered_lines = []
def draw(self, commands):
if self.debug_draw:
for cmd in commands:
self.console.print(cmd, end='')
time.sleep(self.debug_draw_delay)
else:
self.console.print(''.join(commands), end='')
def render(self, lines):
if not self.last_rendered_lines:
self.console.print(os.linesep.join(lines), end='')
elif len(lines) != len(self.last_rendered_lines):
self.clear_last_render()
self.render(lines)
else:
redraw_commands = []
last_idx = 0
for idx, new_line in self.changed_lines(lines):
redraw_commands.append(ndk.ansi.cursor_down(idx - last_idx))
redraw_commands.append(ndk.ansi.goto_first_column())
redraw_commands.append(ndk.ansi.clear_line())
redraw_commands.append(new_line)
last_idx = idx
if redraw_commands:
total_lines = len(self.last_rendered_lines)
goto_top = ndk.ansi.cursor_up(total_lines - 1)
goto_bottom = ndk.ansi.cursor_down(total_lines - last_idx - 1)
self.draw([goto_top] + redraw_commands + [goto_bottom])
self.last_rendered_lines = lines
class DumbUiRenderer(UiRenderer):
def __init__(self, console, redraw_rate=30):
super(DumbUiRenderer, self).__init__(console)
self.redraw_rate = redraw_rate
self.last_draw = None
def clear_last_render(self):
pass
def ready_for_draw(self):
if self.last_draw is None:
return True
current_time = time.time()
if current_time - self.last_draw >= self.redraw_rate:
return True
return False
def render(self, lines):
if not self.ready_for_draw():
return
self.console.print(os.linesep.join(lines))
sys.stdout.flush()
self.last_draw = time.time()
class Ui(object):
def __init__(self, ui_renderer):
self.ui_renderer = ui_renderer
def get_ui_lines(self):
raise NotImplementedError
def clear(self):
self.ui_renderer.clear_last_render()
def draw(self):
self.ui_renderer.render(self.get_ui_lines())
def get_build_progress_ui(console, workqueue):
if console.smart_console:
ui_renderer = AnsiUiRenderer(console)
return BuildProgressUi(ui_renderer, workqueue)
else:
return DumbBuildProgressUi()
class BuildProgressUi(Ui):
def __init__(self, ui_renderer, workqueue):
super(BuildProgressUi, self).__init__(ui_renderer)
self.workqueue = workqueue
def get_ui_lines(self):
lines = []
for worker in self.workqueue.workers:
status = worker.status
if status != worker.IDLE_STATUS:
lines.append(status)
return lines
class DumbBuildProgressUi(object):
def clear(self):
pass
def draw(self):
# Don't flood the terminal with repeated status of what is still
# building. it will be printing the same three modules for most of the
# build.
pass
def get_work_queue_ui(console, workqueue):
if console.smart_console:
ui_renderer = ndk.ui.AnsiUiRenderer(console)
show_worker_status = True
else:
ui_renderer = ndk.ui.DumbUiRenderer(console)
show_worker_status = False
return WorkQueueUi(
ui_renderer, show_worker_status, workqueue)
def columnate(lines, max_width, max_height):
if os.name == 'nt':
# Not yet implemented.
return lines
num_columns = int(math.ceil(len(lines) / max_height))
if num_columns == 1:
return lines
# Keep the columns roughly balanced.
num_rows = int(math.ceil(len(lines) / num_columns))
rows = [lines[r::num_rows] for r in range(num_rows)]
column_width = max_width // num_columns
return [''.join(s.ljust(column_width) for s in row) for row in rows]
class WorkQueueUi(Ui):
NUM_TESTS_DIGITS = 6
def __init__(self, ui_renderer, show_worker_status, workqueue):
super(WorkQueueUi, self).__init__(ui_renderer)
self.show_worker_status = show_worker_status
self.workqueue = workqueue
def get_ui_lines(self):
lines = []
if self.show_worker_status:
for worker in self.workqueue.workers:
lines.append(worker.status)
if self.ui_renderer.console.smart_console:
# Keep some space at the top of the UI so we can see messages.
ui_height = self.ui_renderer.console.height - 10
if ui_height > 0:
lines = columnate(lines, self.ui_renderer.console.width,
ui_height)
lines.append('{: >{width}} jobs remaining'.format(
self.workqueue.num_tasks, width=self.NUM_TESTS_DIGITS))
return lines