Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend: add failed-to-succeeded-stats command #2798

Merged
merged 1 commit into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions frontend/coprs_frontend/commands/failed_to_succeeded_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Generate failed to succeeded stats
"""

import os
from datetime import datetime

import click
import pygal
from coprs import models


@click.command()
@click.option("--dest", "-D", required=True, help="Result directory")
def failed_to_succeeded_stats(dest):
"""
Generate failed to succeeded stats
"""
print("Please wait, this will take at least 20 minutes.")
categories = {
"immediately": 0,
"seconds": 0,
"minutes": 0,
"hours": 0,
"days": 0,
"weeks": 0,
}
tuples = failed_to_succeeded_tuples()
for failed, succeeded in tuples:
delta = datetime.fromtimestamp(succeeded) \
- datetime.fromtimestamp(failed)
categories[delta_to_category(delta)] += 1

os.makedirs(dest, exist_ok=True)
path = os.path.join(dest, "failed-to-succeeded-stats.svg")
generate_graph(categories, path)
print("Created: {0}".format(path))


def get_builds():
"""
Return list of all builds
"""
query = (
models.Build.query
.join(models.Package)
.join(models.Copr)
.order_by(models.Build.id)
)

# Only recent builds
# start = datetime(2022, 1, 1).timestamp()
# end = datetime(2023, 1, 1).timestamp()
# query = (query
# .filter(models.Build.submitted_on >= start)
# .filter(models.Build.submitted_on <= end))

# Packit user
# query = query.filter(models.Copr.user_id==5576)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with this, are the builds filtered via the Copr project owner? (or via the submitter of the build?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaik the user_id in Copr model is only project owner since this model represents the project as a whole, not the individual builds. To filter builds according to submitter:

query = query.filter(models.Build.user_id=PACKIT_USER_ID)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly as @nikromen is saying.


# For faster development
# query = query.limit(1000)

return query.all()


def builds_per_package():
"""
Return a `dict` where keys are package IDs and values are lists
of all their builds.
"""
builds = get_builds()
result = {}
for build in builds:
result.setdefault(build.package_id, [])
result[build.package_id].append(build)
return result


def failed_to_succeeded_tuples():
"""
Return a list of tuples. First value of each tuple is when the package
failed, and the second value is when it succeeded.
"""
tuples = []
for builds in builds_per_package().values():
if len(builds) <= 1:
# This package has only one build
# Not dealing with this now.
continue

failed = None
succeeded = None

for build in builds:
if not build.ended_on:
continue

if build.state == "failed" and not failed:
failed = build

elif build.state == "succeeded" and failed:
succeeded = build

if failed and succeeded:
assert failed.id < succeeded.id
tuples.append((failed.ended_on, succeeded.ended_on))
failed = None
succeeded = None
return tuples


def delta_to_category(delta):
"""
Convert timedelta into a custom time category
"""
seconds = delta.total_seconds()
if seconds < 0:
return "immediately"
if seconds < 60:
return "seconds"
if seconds < 60 * 60:
return "minutes"
if seconds < 60 * 60 * 24:
return "hours"
if seconds < 60 * 60 * 24 * 7:
return "days"
return "weeks"


def generate_graph(data, path):
"""
Generate graph from the data
"""
title = "How long before devs submit a successfull package after a failure?"
chart = pygal.Bar(
title=title,
print_values=True,
legend_at_bottom=True,
)
for key, value in data.items():
label = label_for_group(key)
chart.add(label, value)
chart.render_to_file(path)
return path


def label_for_group(key):
"""
User-friendly labels for the graph
"""
if key == "immediately":
return "Before it finished"
return key.capitalize()
2 changes: 2 additions & 0 deletions frontend/coprs_frontend/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import commands.chroots_template
import commands.warning_banner
import commands.usage_treemap
import commands.failed_to_succeeded_stats

from coprs import app

Expand Down Expand Up @@ -94,6 +95,7 @@
"delete_dirs",
"warning_banner",
"usage_treemap",
"failed_to_succeeded_stats",
]


Expand Down