Skip to content

Commit

Permalink
feat(performance): move build status count to on demand
Browse files Browse the repository at this point in the history
- Added a new endpoint to fetch build status counts
- Created `BuildStatusCountView` in `views/buildStatusCountView.py`
- Updated `urls.py` to include the new endpoint
- Modified `TreeDetails.tsx` to use the new endpoint
- Updated `Accordion` component to display build status counts
- Refactored `TreeDetails` related types and hooks
  • Loading branch information
WilsonNet committed Sep 12, 2024
1 parent 955c456 commit f6e3e44
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 173 deletions.
1 change: 1 addition & 0 deletions backend/kernelCI_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
path(
"tree/<str:commit_hash>/tests/", views.TreeTestsView.as_view(), name="treeTests"
),
path("build/<str:build_id>/status-count", views.BuildStatusCountView.as_view(), name="buildStatusCount"),
path("build/<str:build_id>", views.BuildDetails.as_view(), name="buildDetails"),
path("build/<str:build_id>/tests", views.BuildTests.as_view(), name="buildTests"),
path("tests/test/<str:test_id>", views.TestDetails.as_view(), name="testDetails"),
Expand Down
48 changes: 48 additions & 0 deletions backend/kernelCI_app/views/buildStatusCountView.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.http import JsonResponse
from rest_framework.views import APIView
from kernelCI_app.models import Builds


class BuildStatusCountView(APIView):
def get(self, _request, build_id):
builds = Builds.objects.raw(
"""
SELECT
builds.id,
COUNT(CASE WHEN tests.status = 'FAIL' THEN 1 END) AS fail_tests,
COUNT(CASE WHEN tests.status = 'ERROR' THEN 1 END) AS error_tests,
COUNT(CASE WHEN tests.status = 'MISS' THEN 1 END) AS miss_tests,
COUNT(CASE WHEN tests.status = 'PASS' THEN 1 END) AS pass_tests,
COUNT(CASE WHEN tests.status = 'DONE' THEN 1 END) AS done_tests,
COUNT(CASE WHEN tests.status = 'SKIP' THEN 1 END) AS skip_tests,
SUM(CASE WHEN tests.status IS NULL AND tests.id IS NOT NULL THEN 1 ELSE 0 END)
AS null_tests,
COUNT(tests.id) AS total_tests
FROM
builds
LEFT JOIN
tests ON tests.build_id = builds.id
WHERE builds.id = %s
GROUP BY builds.id;
""",
[build_id],
)

build_status_counts = list(builds)
if not build_status_counts:
return JsonResponse({"error": "Build not found"}, status=404)

build_status = build_status_counts[0]
build_counts = {
"build_id": build_status.id,
"fail_tests": build_status.fail_tests,
"error_tests": build_status.error_tests,
"miss_tests": build_status.miss_tests,
"pass_tests": build_status.pass_tests,
"done_tests": build_status.done_tests,
"skip_tests": build_status.skip_tests,
"null_tests": build_status.null_tests,
"total_tests": build_status.total_tests,
}

return JsonResponse({"build_counts": build_counts})
103 changes: 47 additions & 56 deletions backend/kernelCI_app/views/treeDetailsView.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,95 +11,86 @@ def create_default_status(self):
return {"valid": 0, "invalid": 0, "null": 0}

def create_summary(self, builds_dict):
status_map = {True: 'valid', False: 'invalid', None: 'null'}
status_map = {True: "valid", False: "invalid", None: "null"}

build_summ = self.create_default_status()
config_summ = {}
arch_summ = {}

for build in builds_dict:
status_key = status_map[build['valid']]
status_key = status_map[build["valid"]]
build_summ[status_key] += 1

if config := build['config_name']:
if config := build["config_name"]:
status = config_summ.get(config)
if not status:
status = self.create_default_status()
config_summ[config] = status
status[status_key] += 1

if arch := build['architecture']:
status = arch_summ.setdefault(
arch, self.create_default_status())
if arch := build["architecture"]:
status = arch_summ.setdefault(arch, self.create_default_status())
status[status_key] += 1
compiler = build['compiler']
if compiler and compiler not in status.setdefault('compilers', []):
status['compilers'].append(compiler)

return {"builds": build_summ, "configs": config_summ, "architectures": arch_summ}

def get_test_status(self, build_id):
status_keys = ["fail_tests", "error_tests", "miss_tests", "pass_tests",
"done_tests", "skip_tests", "null_tests", "total_tests"]
builds = Builds.objects.raw(
"""
SELECT
builds.id,
COUNT(CASE WHEN tests.status = 'FAIL' THEN 1 END) AS fail_tests,
COUNT(CASE WHEN tests.status = 'ERROR' THEN 1 END) AS error_tests,
COUNT(CASE WHEN tests.status = 'MISS' THEN 1 END) AS miss_tests,
COUNT(CASE WHEN tests.status = 'PASS' THEN 1 END) AS pass_tests,
COUNT(CASE WHEN tests.status = 'DONE' THEN 1 END) AS done_tests,
COUNT(CASE WHEN tests.status = 'SKIP' THEN 1 END) AS skip_tests,
SUM(CASE WHEN tests.status IS NULL AND tests.id IS NOT NULL THEN 1 ELSE 0 END)
AS null_tests,
COUNT(tests.id) AS total_tests
FROM
builds
LEFT JOIN
tests ON tests.build_id = builds.id
WHERE builds.id = %s
GROUP BY builds.id;
""",
[build_id]
)
return {k: getattr(builds[0], k) for k in status_keys}
compiler = build["compiler"]
if compiler and compiler not in status.setdefault("compilers", []):
status["compilers"].append(compiler)

return {
"builds": build_summ,
"configs": config_summ,
"architectures": arch_summ,
}

def get(self, request, commit_hash):
build_fields = [
'id', 'architecture', 'config_name', 'misc', 'config_url',
'compiler', 'valid', 'duration', 'log_url', 'start_time']
"id",
"architecture",
"config_name",
"misc",
"config_url",
"compiler",
"valid",
"duration",
"log_url",
"start_time",
]
checkout_fields = [
'git_repository_branch', 'git_repository_url', 'git_repository_branch']

query = Query().from_table(Builds, build_fields).join(
'checkouts',
condition='checkouts.id = builds.checkout_id',
fields=checkout_fields
).where(git_commit_hash__eq=commit_hash)
"git_repository_branch",
"git_repository_url",
"git_repository_branch",
]

query = (
Query()
.from_table(Builds, build_fields)
.join(
"checkouts",
condition="checkouts.id = builds.checkout_id",
fields=checkout_fields,
)
.where(git_commit_hash__eq=commit_hash)
)

try:
filter_params = FilterParams(request)
except InvalidComparisonOP as e:
return HttpResponseBadRequest(getErrorResponseBody(str(e)))

for f in filter_params.filters:
field = f['field']
field = f["field"]
table = None
if field in build_fields:
table = 'builds'
table = "builds"
elif field in checkout_fields:
table = 'checkouts'
table = "checkouts"
if table:
op = filter_params.get_comparison_op(f, 'orm')
query.where(
**{f'{table}.{field}__{op}': f['value']})
op = filter_params.get_comparison_op(f, "orm")
query.where(**{f"{table}.{field}__{op}": f["value"]})

records = query.select()
for r in records:
status = self.get_test_status(r.get('id'))
r['status'] = status

summary = self.create_summary(records)

print("Summary: ", summary)

return JsonResponse({"builds": records, "summary": summary}, safe=False)
33 changes: 29 additions & 4 deletions dashboard/src/api/TreeDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query';

import {
TTreeTestsData,
TreeDetails,
BootsTab,
TTreeDetailsFilter,
TTestByCommitHashResponse,
TTreeCommitHistoryResponse,
BuildCountsResponse,
} from '@/types/tree/TreeDetails';

import { TPathTests } from '@/types/general';
Expand All @@ -17,7 +18,7 @@ import http from './api';
const fetchTreeDetailData = async (
treeId: string,
filter: TTreeDetailsFilter | Record<string, never>,
): Promise<TreeDetails> => {
): Promise<BootsTab> => {
const filterParam = mapFiltersToUrlSearchParams(filter);

const res = await http.get(`/api/tree/${treeId}`, { params: filterParam });
Expand All @@ -39,10 +40,10 @@ const mapFiltersToUrlSearchParams = (
return filterParam;
};

export const useTreeDetails = (
export const useBuildsTab = (
treeId: string,
filter: TTreeDetailsFilter | Record<string, never> = {},
): UseQueryResult<TreeDetails> => {
): UseQueryResult<BootsTab> => {
const detailsFilter = getTargetFilter(filter, 'treeDetails');

return useQuery({
Expand Down Expand Up @@ -235,3 +236,27 @@ export const useTreeCommitHistory = (
fetchTreeCommitHistory(commitHash, origin, gitUrl, gitBranch),
});
};

const fetchBuildStatusCount = async (
buildId: string,
): Promise<BuildCountsResponse> => {
const res = await http.get<BuildCountsResponse>(
`/api/build/${buildId}/status-count`,
);
return res.data;
};

export const useBuildStatusCount = (
{
buildId,
}: {
buildId: string;
},
{ enabled = true },
): UseQueryResult<BuildCountsResponse> => {
return useQuery({
queryKey: [buildId],
enabled,
queryFn: () => fetchBuildStatusCount(buildId),
});
};
64 changes: 35 additions & 29 deletions dashboard/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,36 +110,42 @@ const AccordionTableBody = ({
</div>
);
}
return items.map((item, index) => (
<Collapsible key={index} asChild>
<>
<CollapsibleTrigger asChild>
<TableRow className="group cursor-pointer hover:bg-lightBlue">
{type === 'build' ? (
<AccordionBuildsTrigger accordionData={item} />
) : (
<AccordionTestsTrigger accordionData={item} />
)}
return items.map(item => {
const itemKey =
type === 'build'
? (item as AccordionItemBuilds).id
: (item as TPathTests).path_group;
return (
<Collapsible key={itemKey} asChild>
<>
<CollapsibleTrigger asChild>
<TableRow className="group cursor-pointer hover:bg-lightBlue">
{type === 'build' ? (
<AccordionBuildsTrigger accordionData={item} />
) : (
<AccordionTestsTrigger accordionData={item} />
)}
</TableRow>
</CollapsibleTrigger>
<TableRow>
<TableCell colSpan={6} className="p-0">
<CollapsibleContent>
<div className="group max-h-[400px] w-full overflow-scroll border-b border-darkGray bg-lightGray p-8">
{type === 'build' ? (
<AccordionBuildContent accordionData={item} />
) : (
<AccordionTestsContent
data={(item as TPathTests).individual_tests}
/>
)}
</div>
</CollapsibleContent>
</TableCell>
</TableRow>
</CollapsibleTrigger>
<TableRow>
<TableCell colSpan={6} className="p-0">
<CollapsibleContent>
<div className="group max-h-[400px] w-full overflow-scroll border-b border-darkGray bg-lightGray p-8">
{type === 'build' ? (
<AccordionBuildContent accordionData={item} />
) : (
<AccordionTestsContent
data={(item as TPathTests).individual_tests}
/>
)}
</div>
</CollapsibleContent>
</TableCell>
</TableRow>
</>
</Collapsible>
));
</>
</Collapsible>
);
});
}, [items, type]);

return <TableBody>{accordionItems}</TableBody>;
Expand Down
Loading

0 comments on commit f6e3e44

Please sign in to comment.