]> git.ipfire.org Git - pbs.git/commitdiff
builders: Refactor detail page and add live statistics
authorMichael Tremer <michael.tremer@ipfire.org>
Sun, 30 Apr 2023 16:05:19 +0000 (16:05 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Sun, 30 Apr 2023 16:05:19 +0000 (16:05 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/buildservice/builders.py
src/static/js/builders-stats.js [new file with mode: 0644]
src/templates/builders/detail.html
src/templates/builders/modules/stats.html [new file with mode: 0644]
src/web/__init__.py
src/web/builders.py

index fcb7ea11fe460ac56d35b3215868f4fb99f8798e..23593c5c42774d9a7e1e816be3489e01f8e59365 100644 (file)
@@ -172,6 +172,11 @@ dist_templates_builders_DATA = \
 
 templates_buildersdir = $(templatesdir)/builders
 
+dist_templates_builders_modules_DATA = \
+       src/templates/builders/modules/stats.html
+
+templates_builders_modulesdir = $(templates_buildersdir)/modules
+
 dist_templates_builds_DATA = \
        src/templates/builds/delete.html \
        src/templates/builds/index.html \
@@ -324,6 +329,7 @@ CLEANFILES += \
        src/static/css/site.css
 
 static_js_DATA = \
+       src/static/js/builders-stats.min.js \
        src/static/js/jquery.min.js \
        src/static/js/job-log-stream.min.js \
        src/static/js/pbs.min.js
@@ -331,6 +337,7 @@ static_js_DATA = \
 static_jsdir = $(staticdir)/js
 
 EXTRA_DIST += \
+       src/static/js/builders-stats.js \
        src/static/js/job-log-stream.js \
        src/static/js/pbs.js
 
index 1da651030811e4b2d47f06740a532f692f8f3a20..2c33f4de4d6fee8fa9ae14fe9c6dcbaa4b477529 100644 (file)
@@ -35,6 +35,10 @@ class Builders(base.Object):
 
                return iter(builders)
 
+       def init(self):
+               # Initialize stats
+               self.stats = BuildersStats(self.backend)
+
        def create(self, name, user=None, log=True):
                """
                        Creates a new builder.
@@ -199,6 +203,41 @@ class Builders(base.Object):
                return { row.arch : row.t for row in res }
 
 
+class BuildersStats(base.Object):
+       builders = {}
+
+       def join(self, builder, connection):
+               try:
+                       self.builders[builder].append(connection)
+               except KeyError:
+                       self.builders[builder] = [connection]
+
+       def leave(self, connection):
+               # Find and remove the connection
+               for builder in self.builders.copy():
+                       try:
+                               self.builders[builder].remove(connection)
+                       except IndexError:
+                               continue
+
+                       # Remove the builder if it has no connections left
+                       if not self.builders[builder]:
+                               del self.builders[builder]
+
+       async def submit_stats(self, builder, stats):
+               """
+                       Called when a builder sends new stats
+               """
+               try:
+                       connections = self.builders[builder]
+               except KeyError:
+                       return
+
+               # Send the stats to all connections
+               await asyncio.gather(
+                       *(c.submit_stats(stats) for c in connections),
+               )
+
 
 class Builder(base.DataObject):
        table = "builders"
@@ -240,7 +279,15 @@ class Builder(base.DataObject):
 
                return True
 
-       def log_stats(self, cpu_model=None, cpu_count=None, cpu_arch=None, pakfire_version=None,
+       # Stats
+
+       def _get_stats(self, query, *args, **kwargs):
+               res = self.db.get(query, *args, **kwargs)
+
+               if res:
+                       return BuilderStats(self.backend, self, res)
+
+       async def log_stats(self, cpu_model=None, cpu_count=None, cpu_arch=None, pakfire_version=None,
                        os_name=None, cpu_user=None, cpu_nice=None, cpu_system=None, cpu_idle=None,
                        cpu_iowait=None, cpu_irq=None, cpu_softirq=None, cpu_steal=None, cpu_guest=None,
                        cpu_guest_nice=None, loadavg1=None, loadavg5=None, loadavg15=None, mem_total=None,
@@ -272,7 +319,7 @@ class Builder(base.DataObject):
                )
 
                # Log Stats
-               self.db.execute("""
+               stats = self._get_stats("""
                        INSERT INTO
                                builder_stats
                        (
@@ -306,7 +353,8 @@ class Builder(base.DataObject):
                        VALUES (
                                %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
                                %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
-                       )""",
+                       )
+                       RETURNING *""",
                        self.id,
                        cpu_user,
                        cpu_nice,
@@ -335,12 +383,15 @@ class Builder(base.DataObject):
                        swap_free,
                )
 
+               # Send out the stats
+               await self.backend.builders.stats.submit_stats(self, stats)
+
        @lazy_property
        def stats(self):
                """
                        Returns the latest stats data (if any)
                """
-               return self.db.get("""
+               return self._get_stats("""
                        SELECT
                                *
                        FROM
@@ -713,3 +764,30 @@ class Builder(base.DataObject):
                                "pkg"     : job.pkg.download_url,
                        },
                })
+
+
+class BuilderStats(base.Object):
+       def init(self, builder, data):
+               self.builder = builder
+               self.data = data
+
+       @property
+       def cpu_usage(self):
+               """
+                       Returns the CPU usage in percent
+               """
+               return 100 - self.data.cpu_idle
+
+       @property
+       def mem_usage(self):
+               """
+                       Returns the amount of used memory in percent
+               """
+               return (self.data.mem_total - self.data.mem_available) / self.data.mem_total * 100
+
+       @property
+       def swap_usage(self):
+               """
+                       Returns the amount of used swap in percent
+               """
+               return self.data.swap_used / self.data.swap_total * 100
diff --git a/src/static/js/builders-stats.js b/src/static/js/builders-stats.js
new file mode 100644 (file)
index 0000000..81d5efc
--- /dev/null
@@ -0,0 +1,52 @@
+$(".builders-stats").each(function() {
+       // Fetch the name of the builder
+       const name = $(this).data("name");
+
+       const cpu_usage  = $(this).find("#cpu-usage");
+       const mem_usage  = $(this).find("#mem-usage");
+       const swap_usage = $(this).find("#swap-usage");
+
+       // Make the URL
+       const url = "wss://" + window.location.host + "/builders/" + name + "/stats";
+
+       // Try to connect to the stream
+       const stream = new WebSocket(url);
+
+       // Updates the progressbar and sets a colour
+       updateProgressBar = function(e, percentage) {
+               // Set the value
+               e.val(percentage);
+
+               // Remove all classes
+               e.removeClass("is-dark is-light is-danger is-warning is-success");
+
+               if (percentage >= 90) {
+                       e.addClass("is-danger");
+
+               } else if (percentage >= 75) {
+                       e.addClass("is-warning");
+
+               } else {
+                       e.addClass("is-success");
+               }
+       }
+
+       stream.addEventListener("message", (event) => {
+               // Parse message as JSON
+               const data = JSON.parse(event.data);
+
+               console.debug("Message from server: ", data);
+
+               // Set CPU usage
+               if (cpu_usage)
+                       updateProgressBar(cpu_usage, data.cpu_usage);
+
+               // Set memory usage
+               if (mem_usage)
+                       updateProgressBar(mem_usage, data.mem_usage);
+
+               // Set swap usage
+               if (swap_usage)
+                       updateProgressBar(swap_usage, data.swap_usage);
+       });
+});
index 3cdfbe450eccde09691f679622b174c78b261fe1..d1d88bbd610459428bf84442a8f1e55694340255 100644 (file)
                </ul>
        </nav>
 
-       <h1 class="title is-1">
-               {{ builder }}
-
-               {% if builder.testmode %}
-                       <span class="tag is-warning">{{ _("Test Mode") }}</span>
+       <section class="section">
+               <h1 class="title is-1">{{ builder }}</h1>
+               {% if builder.cpu_model %}
+                       <h4 class="subtitle is-4">
+                               {{ builder.cpu_model or _("Unknown CPU Model") }}
+                               {% if builder.cpu_count > 1 %}
+                                       &times; {{ builder.cpu_count }}
+                               {% end %}
+
+                               <span class="tag">
+                                       {{ builder.arch }}
+                               </span>
+                       </h4>
                {% end %}
-       </h1>
 
-       {% if builder.has_perm(current_user) %}
                <div class="block">
-                       <a class="button is-warning is-small" href="/builders/{{ builder.hostname }}/edit">
-                               {{ _("Edit Builder") }}
-                       </a>
-               </div>
-       {% end %}
-
-       <div class="block">
-               <div class="box">
-                       <div class="columns">
-                               <div class="column is-8">
-                                       {# XXX THIS IS ALL BORING AND UGLY BUT NOT TOO IMPORTANT RIGHT NOW #}
-
-                                       {% if builder.cpu_model %}
-                                               <p>
-                                                       {{ builder.cpu_model or _("Unknown CPU Model") }}
-                                                       {% if builder.cpu_count > 1 %}
-                                                               &times; {{ builder.cpu_count }}
-                                                       {% end %}
-                                               </p>
-                                       {% end %}
-
-                                       {% if builder.pakfire_version %}
-                                               <p>
-                                                       {{ _("Pakfire %s") % builder.pakfire_version }}
-                                               </p>
-                                       {% end %}
-
-                                       {% if builder.os_name %}
-                                               <p>
-                                                       {{ builder.os_name }}
-                                               </p>
-                                       {% end %}
-
-                                       <p>
-                                               {{ _("Supported Architectures: %s") % locale.list(builder.supported_arches) }}
-                                       </p>
-
-                                       {% if builder.total_build_time %}
-                                               <p>
-                                                       {{ _("Total Build Time: %s") % format_time(builder.total_build_time) }}
-                                               </p>
-                                       {% end %}
-
-                                       {% if builder.stats %}
-                                               <p>
-                                                       {{ _("%s Memory") % format_size(builder.stats.mem_total) }}
-                                               </p>
+                       <div class="box">
+                               <div class="block">
+                                       <nav class="level">
+                                               {% if builder.total_build_time %}
+                                                       <div class="level-item has-text-centered">
+                                                               <div>
+                                                                       <p class="heading">{{ _("Total Build Time") }}</p>
+                                                                       <p class="title">
+                                                                               {{ format_time(builder.total_build_time) }}
+                                                                       </p>
+                                                               </div>
+                                                       </div>
+                                               {% end %}
 
-                                               {% if builder.stats.swap_total %}
-                                                       <p>
-                                                               {{ _("%s Swap Space") % format_size(builder.stats.swap_total) }}
-                                                       </p>
+                                               {% if builder.pakfire_version %}
+                                                       <div class="level-item has-text-centered">
+                                                               <div>
+                                                                       <p class="heading">{{ _("Pakfire Version") }}</p>
+                                                                       <p class="title">
+                                                                               {{ builder.pakfire_version }}
+                                                                       </p>
+                                                               </div>
+                                                       </div>
                                                {% end %}
-                                       {% end %}
 
-                                       {% if builder.description %}
-                                               {% module Text(builder.description) %}
-                                       {% end %}
+                                               {% if builder.os_name %}
+                                                       <div class="level-item has-text-centered">
+                                                               <div>
+                                                                       <p class="heading">{{ _("Operating System") }}</p>
+                                                                       <p class="title">
+                                                                               {{ builder.os_name }}
+                                                                       </p>
+                                                               </div>
+                                                       </div>
+                                               {% end %}
+                                       </nav>
                                </div>
 
-                               <div class="column is-4">
-                                       <h1>
-                                               {{ len(builder.jobs) }}
-                                               <small>/ {{ builder.max_jobs }}</small>
-                                       </h1>
-                                       <h5>{{ _("Jobs") }}</h5>
+                               {# Builder Stats #}
+                               <div class="block">
+                                       {% module BuilderStats(builder) %}
                                </div>
+
+
+                               {% if builder.description %}
+                                       {% module Text(builder.description) %}
+                               {% end %}
                        </div>
                </div>
-       </div>
 
-       <!-- XXX add some realtime graph -->
+               {% if builder.has_perm(current_user) %}
+                       <div class="block">
+                               <a class="button is-warning" href="/builders/{{ builder.hostname }}/edit">
+                                       {{ _("Edit") }}
+                               </a>
+                       </div>
+               {% end %}
+       </section>
 
        {% if builder.jobs %}
-               <div class="block">
+               <section class="section">
                        <h5 class="subtitle is-5">{{ _("Running Jobs") }}</h5>
 
                        {% module JobsList(builder.jobs) %}
-               </div>
+               </section>
        {% end %}
 
-       <div class="block">
+       <section class="section">
                <h5 class="title is-5">{{ _("Log") }}</h5>
 
                {% module EventsList(builder=builder, show_builder=False, limit=10) %}
-       </div>
+       </section>
 {% end block %}
diff --git a/src/templates/builders/modules/stats.html b/src/templates/builders/modules/stats.html
new file mode 100644 (file)
index 0000000..689478a
--- /dev/null
@@ -0,0 +1,31 @@
+<div class="builders-stats" data-name="{{ builder.name }}">
+       {% if builder.is_online() %}
+               <div class="columns is-vcentered">
+                       <div class="column is-2">
+                               {{ _("Processor") }}
+                       </div>
+
+                       <div class="column">
+                               <progress class="progress is-dark" id="cpu-usage" max="100"></progress>
+                       </div>
+               </div>
+
+               <div class="columns is-vcentered">
+                       <div class="column is-2">
+                               {{ _("Memory") }}
+                       </div>
+
+                       <div class="column">
+                               <progress class="progress is-small is-dark" id="mem-usage" max="100"></progress>
+                       </div>
+
+                       <div class="column is-2">
+                               {{ _("Swap Usage") }}
+                       </div>
+
+                       <div class="column">
+                               <progress class="progress is-small is-dark" id="swap-usage" max="100"></progress>
+                       </div>
+               </div>
+       {% end %}
+</div>
index 795c24eaaa7b96a66d7d8c432d99bf4d956f6ba8..efa04c761260c371d086278b964f784ed8fef3dc 100644 (file)
@@ -46,6 +46,9 @@ class Application(tornado.web.Application):
                                # Builds
                                "BuildsList"         : builds.ListModule,
 
+                               # Builders
+                               "BuilderStats"       : builders.StatsModule,
+
                                # Distros
                                "DistrosList"        : distributions.ListModule,
 
@@ -151,6 +154,7 @@ class Application(tornado.web.Application):
                        (r"/builders/([A-Za-z0-9\-\.]+)/delete", builders.BuilderDeleteHandler),
                        (r"/builders/([A-Za-z0-9\-\.]+)/edit", builders.BuilderEditHandler),
                        (r"/builders/([A-Za-z0-9\-\.]+)", builders.BuilderDetailHandler),
+                       (r"/builders/([A-Za-z0-9\-\.]+)/stats", builders.StatsHandler),
                        (r"/api/v1/builders/control", builders.APIv1ControlHandler),
 
                        # Distributions
index 2d27e62afeb96fc498034ee171342ff0aa6d7a55..11f6b55e2cfbfbc072c6d1b8a238baa2a0c81945 100644 (file)
@@ -4,6 +4,7 @@ import logging
 import tornado.web
 
 from . import base
+from . import ui_modules
 
 # Setup logging
 log = logging.getLogger("pbs.web.builders")
@@ -24,7 +25,7 @@ class APIv1ControlHandler(base.APIMixin, tornado.websocket.WebSocketHandler):
                # Drop the connection to the builder
                self.current_user.disconnected()
 
-       def on_message(self, message):
+       async def on_message(self, message):
                # Decode message
                message = self._decode_json_message(message)
 
@@ -34,18 +35,44 @@ class APIv1ControlHandler(base.APIMixin, tornado.websocket.WebSocketHandler):
 
                # Handle stats
                if type == "stats":
-                       self._handle_stats(data)
+                       await self._handle_stats(data)
 
                # Log an error and ignore any other messages
                else:
                        log.error("Received message of type '%s' which we cannot handle here" % type)
 
-       def _handle_stats(self, data):
+       async def _handle_stats(self, data):
                """
                        Handles stats messages
                """
                with self.db.transaction():
-                       self.builder.log_stats(**data)
+                       await self.builder.log_stats(**data)
+
+
+class StatsHandler(base.BackendMixin, tornado.websocket.WebSocketHandler):
+       # No authentication required
+       async def open(self, name):
+               builder = self.backend.builders.get_by_name(name)
+               if not builder:
+                       raise tornado.web.HTTPError(404, "Could not find builder %s" % name)
+
+               # Register to receive updates
+               self.backend.builders.stats.join(builder=builder, connection=self)
+
+               # Initially send the stats that we currently have
+               if builder.stats:
+                       await self.submit_stats(builder.stats)
+
+       def on_close(self):
+               self.backend.builders.stats.leave(self)
+
+       async def submit_stats(self, stats):
+               await self.write_message({
+                       "cpu_usage"  : stats.cpu_usage,
+                       "mem_usage"  : stats.mem_usage,
+                       "swap_usage" : stats.swap_usage,
+               })
+
 
 
 class BuilderListHandler(base.BaseHandler):
@@ -131,3 +158,13 @@ class BuilderDeleteHandler(base.BaseHandler):
                        return
 
                self.render("builders/delete.html", builder=builder)
+
+
+class StatsModule(ui_modules.UIModule):
+       def render(self, builder):
+               return self.render_string("builders/modules/stats.html", builder=builder)
+
+       def javascript_files(self):
+               return (
+                       "js/builders-stats.min.js",
+               )